edq-utils 0.0.5__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of edq-utils might be problematic. Click here for more details.
- edq/__init__.py +1 -1
- edq/cli/config/__init__.py +3 -0
- edq/cli/config/list.py +69 -0
- edq/cli/http/__init__.py +3 -0
- edq/cli/http/exchange-server.py +71 -0
- edq/cli/http/send-exchange.py +45 -0
- edq/cli/http/verify-exchanges.py +38 -0
- edq/cli/testing/__init__.py +3 -0
- edq/cli/testing/cli-test.py +8 -5
- edq/cli/version.py +2 -1
- edq/core/argparser.py +28 -3
- edq/core/config.py +268 -0
- edq/core/config_test.py +1038 -0
- edq/procedure/__init__.py +0 -0
- edq/procedure/verify_exchanges.py +85 -0
- edq/testing/asserts.py +0 -1
- edq/testing/cli.py +17 -1
- edq/testing/httpserver.py +553 -0
- edq/testing/httpserver_test.py +424 -0
- edq/testing/run.py +40 -10
- edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
- edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
- edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
- edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
- edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
- edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
- edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
- edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
- edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
- edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
- edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
- edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
- edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
- edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
- edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
- edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
- edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
- edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
- edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
- edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
- edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
- edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
- edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
- edq/testing/testdata/http/files/a.txt +1 -0
- edq/testing/testdata/http/files/tiny.png +0 -0
- edq/testing/unittest.py +12 -7
- edq/util/dirent.py +2 -0
- edq/util/json.py +21 -4
- edq/util/net.py +894 -0
- edq_utils-0.0.6.dist-info/METADATA +156 -0
- edq_utils-0.0.6.dist-info/RECORD +78 -0
- edq_utils-0.0.5.dist-info/METADATA +0 -63
- edq_utils-0.0.5.dist-info/RECORD +0 -34
- {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/WHEEL +0 -0
- {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {edq_utils-0.0.5.dist-info → edq_utils-0.0.6.dist-info}/top_level.txt +0 -0
edq/__init__.py
CHANGED
edq/cli/config/list.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
List the current configuration options.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import edq.core.argparser
|
|
9
|
+
|
|
10
|
+
CONFIG_FIELD_SEPARATOR: str = "\t"
|
|
11
|
+
|
|
12
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
13
|
+
""" Run the CLI. """
|
|
14
|
+
|
|
15
|
+
rows = []
|
|
16
|
+
|
|
17
|
+
for (key, value) in args._config.items():
|
|
18
|
+
row = [key, str(value)]
|
|
19
|
+
if (args.show_origin):
|
|
20
|
+
config_source_obj = args._config_sources.get(key)
|
|
21
|
+
|
|
22
|
+
origin = config_source_obj.path
|
|
23
|
+
if (origin is None):
|
|
24
|
+
origin = config_source_obj.label
|
|
25
|
+
|
|
26
|
+
row.append(origin)
|
|
27
|
+
|
|
28
|
+
rows.append(CONFIG_FIELD_SEPARATOR.join(row))
|
|
29
|
+
|
|
30
|
+
rows.sort()
|
|
31
|
+
|
|
32
|
+
if (not args.skip_header):
|
|
33
|
+
header = ["Key", "Value"]
|
|
34
|
+
if (args.show_origin):
|
|
35
|
+
header.append("Origin")
|
|
36
|
+
|
|
37
|
+
rows.insert(0, (CONFIG_FIELD_SEPARATOR.join(header)))
|
|
38
|
+
|
|
39
|
+
print("\n".join(rows))
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
""" Get a parser, parse the args, and call run. """
|
|
44
|
+
|
|
45
|
+
return run_cli(_get_parser().parse_args())
|
|
46
|
+
|
|
47
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
48
|
+
""" Get a parser and add addition flags. """
|
|
49
|
+
|
|
50
|
+
parser = edq.core.argparser.get_default_parser(__doc__.strip())
|
|
51
|
+
modify_parser(parser)
|
|
52
|
+
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
def modify_parser(parser: argparse.ArgumentParser) -> None:
|
|
56
|
+
""" Add this CLI's flags to the given parser. """
|
|
57
|
+
|
|
58
|
+
parser.add_argument("--show-origin", dest = 'show_origin',
|
|
59
|
+
action = 'store_true',
|
|
60
|
+
help = "Display where each configuration's value was obtained from.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
parser.add_argument("--skip-header", dest = 'skip_header',
|
|
64
|
+
action = 'store_true',
|
|
65
|
+
help = 'Skip headers when displaying configs.',
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (__name__ == '__main__'):
|
|
69
|
+
sys.exit(main())
|
edq/cli/http/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Start an HTTP test server that serves the specified HTTP exchanges.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import edq.core.argparser
|
|
12
|
+
import edq.testing.httpserver
|
|
13
|
+
|
|
14
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
15
|
+
""" Run the CLI. """
|
|
16
|
+
|
|
17
|
+
match_options = {
|
|
18
|
+
'params_to_skip': args.ignore_params,
|
|
19
|
+
'headers_to_skip': args.ignore_headers,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
server = edq.testing.httpserver.HTTPTestServer(
|
|
23
|
+
port = args.port,
|
|
24
|
+
match_options = match_options,
|
|
25
|
+
verbose = True,
|
|
26
|
+
raise_on_404 = False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for path in args.paths:
|
|
30
|
+
path = os.path.abspath(path)
|
|
31
|
+
|
|
32
|
+
if (os.path.isfile(path)):
|
|
33
|
+
server.load_exchange(path)
|
|
34
|
+
else:
|
|
35
|
+
server.load_exchanges_dir(path)
|
|
36
|
+
|
|
37
|
+
server.start_and_wait()
|
|
38
|
+
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
""" Get a parser, parse the args, and call run. """
|
|
43
|
+
return run_cli(_get_parser().parse_args())
|
|
44
|
+
|
|
45
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
46
|
+
""" Get the parser. """
|
|
47
|
+
|
|
48
|
+
parser = edq.core.argparser.get_default_parser(__doc__.strip(),
|
|
49
|
+
include_net = True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
parser.add_argument('paths', metavar = 'PATH',
|
|
53
|
+
type = str, nargs = '+',
|
|
54
|
+
help = 'Path to exchange files or dirs (which will be recursively searched for all exchange files).')
|
|
55
|
+
|
|
56
|
+
parser.add_argument('--port', dest = 'port',
|
|
57
|
+
action = 'store', type = int, default = None,
|
|
58
|
+
help = 'The port to run this test server on. If not set, a random open port will be chosen.')
|
|
59
|
+
|
|
60
|
+
parser.add_argument('--ignore-param', dest = 'ignore_params',
|
|
61
|
+
action = 'append', type = str, default = [],
|
|
62
|
+
help = 'Ignore this parameter during exchange matching.')
|
|
63
|
+
|
|
64
|
+
parser.add_argument('--ignore-header', dest = 'ignore_headers',
|
|
65
|
+
action = 'append', type = str, default = [],
|
|
66
|
+
help = 'Ignore this header during exchange matching.')
|
|
67
|
+
|
|
68
|
+
return parser
|
|
69
|
+
|
|
70
|
+
if (__name__ == '__main__'):
|
|
71
|
+
sys.exit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Send an HTTP exchange to the target server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import edq.core.argparser
|
|
11
|
+
import edq.util.net
|
|
12
|
+
|
|
13
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
14
|
+
""" Run the CLI. """
|
|
15
|
+
|
|
16
|
+
exchange = edq.util.net.HTTPExchange.from_path(args.path)
|
|
17
|
+
_, body = exchange.make_request(args.server)
|
|
18
|
+
|
|
19
|
+
print(body)
|
|
20
|
+
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
""" Get a parser, parse the args, and call run. """
|
|
25
|
+
return run_cli(_get_parser().parse_args())
|
|
26
|
+
|
|
27
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
28
|
+
""" Get the parser. """
|
|
29
|
+
|
|
30
|
+
parser = edq.core.argparser.get_default_parser(__doc__.strip(),
|
|
31
|
+
include_net = True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
parser.add_argument('server', metavar = 'SERVER',
|
|
35
|
+
action = 'store', type = str,
|
|
36
|
+
help = 'Server to send the exahnge to.')
|
|
37
|
+
|
|
38
|
+
parser.add_argument('path', metavar = 'PATH',
|
|
39
|
+
action = 'store', type = str,
|
|
40
|
+
help = 'Path to the exchange file.')
|
|
41
|
+
|
|
42
|
+
return parser
|
|
43
|
+
|
|
44
|
+
if (__name__ == '__main__'):
|
|
45
|
+
sys.exit(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Verify that exchanges sent to a given server have the same response.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import edq.core.argparser
|
|
11
|
+
import edq.procedure.verify_exchanges
|
|
12
|
+
|
|
13
|
+
def run_cli(args: argparse.Namespace) -> int:
|
|
14
|
+
""" Run the CLI. """
|
|
15
|
+
|
|
16
|
+
return edq.procedure.verify_exchanges.run(args.paths, args.server)
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
""" Get a parser, parse the args, and call run. """
|
|
20
|
+
return run_cli(_get_parser().parse_args())
|
|
21
|
+
|
|
22
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
23
|
+
""" Get the parser. """
|
|
24
|
+
|
|
25
|
+
parser = edq.core.argparser.get_default_parser(__doc__.strip())
|
|
26
|
+
|
|
27
|
+
parser.add_argument('server', metavar = 'SERVER',
|
|
28
|
+
action = 'store', type = str, default = None,
|
|
29
|
+
help = 'Address of the server to send exchanges to.')
|
|
30
|
+
|
|
31
|
+
parser.add_argument('paths', metavar = 'PATH',
|
|
32
|
+
type = str, nargs = '+',
|
|
33
|
+
help = 'Path to exchange files or dirs (which will be recursively searched for all exchange files).')
|
|
34
|
+
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
if (__name__ == '__main__'):
|
|
38
|
+
sys.exit(main())
|
edq/cli/testing/__init__.py
CHANGED
edq/cli/testing/cli-test.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pylint: disable=invalid-name
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
4
|
Run specified CLI test files.
|
|
@@ -10,15 +10,18 @@ import unittest
|
|
|
10
10
|
|
|
11
11
|
import edq.core.argparser
|
|
12
12
|
import edq.testing.cli
|
|
13
|
-
import edq.testing.
|
|
13
|
+
import edq.testing.unittest
|
|
14
|
+
|
|
15
|
+
class CLITest(edq.testing.unittest.BaseTest):
|
|
16
|
+
""" Test CLI invocations. """
|
|
14
17
|
|
|
15
18
|
def run_cli(args: argparse.Namespace) -> int:
|
|
16
19
|
""" Run the CLI. """
|
|
17
20
|
|
|
18
|
-
edq.testing.cli.add_test_paths(
|
|
21
|
+
edq.testing.cli.add_test_paths(CLITest, args.data_dir, args.paths)
|
|
19
22
|
|
|
20
23
|
runner = unittest.TextTestRunner(verbosity = 2)
|
|
21
|
-
tests = unittest.defaultTestLoader.loadTestsFromTestCase(
|
|
24
|
+
tests = unittest.defaultTestLoader.loadTestsFromTestCase(CLITest)
|
|
22
25
|
results = runner.run(tests)
|
|
23
26
|
|
|
24
27
|
return len(results.errors) + len(results.failures)
|
|
@@ -27,7 +30,7 @@ def main() -> int:
|
|
|
27
30
|
""" Get a parser, parse the args, and call run. """
|
|
28
31
|
return run_cli(_get_parser().parse_args())
|
|
29
32
|
|
|
30
|
-
def _get_parser() ->
|
|
33
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
31
34
|
""" Get the parser. """
|
|
32
35
|
|
|
33
36
|
parser = edq.core.argparser.get_default_parser(__doc__.strip())
|
edq/cli/version.py
CHANGED
|
@@ -16,9 +16,10 @@ def run_cli(args: argparse.Namespace) -> int:
|
|
|
16
16
|
|
|
17
17
|
def main() -> int:
|
|
18
18
|
""" Get a parser, parse the args, and call run. """
|
|
19
|
+
|
|
19
20
|
return run_cli(_get_parser().parse_args())
|
|
20
21
|
|
|
21
|
-
def _get_parser() ->
|
|
22
|
+
def _get_parser() -> argparse.ArgumentParser:
|
|
22
23
|
""" Get the parser. """
|
|
23
24
|
|
|
24
25
|
return edq.core.argparser.get_default_parser(__doc__.strip())
|
edq/core/argparser.py
CHANGED
|
@@ -8,9 +8,12 @@ while post-callbacks are generally intended to act on the results of parsing.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
+
import functools
|
|
11
12
|
import typing
|
|
12
13
|
|
|
14
|
+
import edq.core.config
|
|
13
15
|
import edq.core.log
|
|
16
|
+
import edq.util.net
|
|
14
17
|
|
|
15
18
|
@typing.runtime_checkable
|
|
16
19
|
class PreParseFunction(typing.Protocol):
|
|
@@ -102,11 +105,33 @@ class Parser(argparse.ArgumentParser):
|
|
|
102
105
|
|
|
103
106
|
return parsed_args # type: ignore[no-any-return]
|
|
104
107
|
|
|
105
|
-
def get_default_parser(description: str
|
|
106
|
-
|
|
108
|
+
def get_default_parser(description: str,
|
|
109
|
+
version: typing.Union[str, None] = None,
|
|
110
|
+
include_log: bool = True,
|
|
111
|
+
include_config: bool = True,
|
|
112
|
+
include_net: bool = False,
|
|
113
|
+
config_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
|
|
114
|
+
) -> Parser:
|
|
115
|
+
""" Get a parser with the requested default callbacks already attached. """
|
|
116
|
+
|
|
117
|
+
if (config_options is None):
|
|
118
|
+
config_options = {}
|
|
107
119
|
|
|
108
120
|
parser = Parser(description = description)
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
if (version is not None):
|
|
123
|
+
parser.add_argument('--version',
|
|
124
|
+
action = 'version', version = version)
|
|
125
|
+
|
|
126
|
+
if (include_log):
|
|
127
|
+
parser.register_callbacks('log', edq.core.log.set_cli_args, edq.core.log.init_from_args)
|
|
128
|
+
|
|
129
|
+
if (include_config):
|
|
130
|
+
config_pre_func = functools.partial(edq.core.config.set_cli_args, **config_options)
|
|
131
|
+
config_post_func = functools.partial(edq.core.config.load_config_into_args, **config_options)
|
|
132
|
+
parser.register_callbacks('config', config_pre_func, config_post_func)
|
|
133
|
+
|
|
134
|
+
if (include_net):
|
|
135
|
+
parser.register_callbacks('net', edq.util.net.set_cli_args, edq.util.net.init_from_args)
|
|
111
136
|
|
|
112
137
|
return parser
|
edq/core/config.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import platformdirs
|
|
6
|
+
|
|
7
|
+
import edq.util.dirent
|
|
8
|
+
import edq.util.json
|
|
9
|
+
|
|
10
|
+
CONFIG_SOURCE_GLOBAL: str = "<global config file>"
|
|
11
|
+
CONFIG_SOURCE_LOCAL: str = "<local config file>"
|
|
12
|
+
CONFIG_SOURCE_CLI_FILE: str = "<cli config file>"
|
|
13
|
+
CONFIG_SOURCE_CLI: str = "<cli argument>"
|
|
14
|
+
|
|
15
|
+
CONFIG_PATHS_KEY: str = 'config_paths'
|
|
16
|
+
CONFIGS_KEY: str = 'configs'
|
|
17
|
+
GLOBAL_CONFIG_KEY: str = 'global_config_path'
|
|
18
|
+
IGNORE_CONFIGS_KEY: str = 'ignore_configs'
|
|
19
|
+
DEFAULT_CONFIG_FILENAME: str = "edq-config.json"
|
|
20
|
+
|
|
21
|
+
class ConfigSource:
|
|
22
|
+
""" A class for storing config source information. """
|
|
23
|
+
|
|
24
|
+
def __init__(self, label: str, path: typing.Union[str, None] = None) -> None:
|
|
25
|
+
self.label = label
|
|
26
|
+
""" The label identifying the config (see CONFIG_SOURCE_* constants). """
|
|
27
|
+
|
|
28
|
+
self.path = path
|
|
29
|
+
""" The path of where the config was sourced from. """
|
|
30
|
+
|
|
31
|
+
def __eq__(self, other: object) -> bool:
|
|
32
|
+
if (not isinstance(other, ConfigSource)):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
return ((self.label == other.label) and (self.path == other.path))
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return f"({self.label}, {self.path})"
|
|
39
|
+
|
|
40
|
+
def get_global_config_path(config_filename: str) -> str:
|
|
41
|
+
""" Get the path for the global config file. """
|
|
42
|
+
|
|
43
|
+
return platformdirs.user_config_dir(config_filename)
|
|
44
|
+
|
|
45
|
+
def get_tiered_config(
|
|
46
|
+
config_filename: str = DEFAULT_CONFIG_FILENAME,
|
|
47
|
+
legacy_config_filename: typing.Union[str, None] = None,
|
|
48
|
+
cli_arguments: typing.Union[dict, argparse.Namespace, None] = None,
|
|
49
|
+
local_config_root_cutoff: typing.Union[str, None] = None,
|
|
50
|
+
) -> typing.Tuple[typing.Dict[str, str], typing.Dict[str, ConfigSource]]:
|
|
51
|
+
"""
|
|
52
|
+
Load all configuration options from files and command-line arguments.
|
|
53
|
+
Returns a configuration dictionary with the values based on tiering rules and a source dictionary mapping each key to its origin.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
if (cli_arguments is None):
|
|
57
|
+
cli_arguments = {}
|
|
58
|
+
|
|
59
|
+
config: typing.Dict[str, str] = {}
|
|
60
|
+
sources: typing.Dict[str, ConfigSource] = {}
|
|
61
|
+
|
|
62
|
+
# Ensure CLI arguments are always a dict, even if provided as argparse.Namespace.
|
|
63
|
+
if (isinstance(cli_arguments, argparse.Namespace)):
|
|
64
|
+
cli_arguments = vars(cli_arguments)
|
|
65
|
+
|
|
66
|
+
global_config_path = cli_arguments.get(GLOBAL_CONFIG_KEY, get_global_config_path(config_filename))
|
|
67
|
+
|
|
68
|
+
# Check the global user config file.
|
|
69
|
+
if (os.path.isfile(global_config_path)):
|
|
70
|
+
_load_config_file(global_config_path, config, sources, CONFIG_SOURCE_GLOBAL)
|
|
71
|
+
|
|
72
|
+
# Check the local user config file.
|
|
73
|
+
local_config_path = _get_local_config_path(
|
|
74
|
+
config_filename = config_filename,
|
|
75
|
+
legacy_config_filename = legacy_config_filename,
|
|
76
|
+
local_config_root_cutoff = local_config_root_cutoff,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (local_config_path is not None):
|
|
80
|
+
_load_config_file(local_config_path, config, sources, CONFIG_SOURCE_LOCAL)
|
|
81
|
+
|
|
82
|
+
# Check the config file specified on the command-line.
|
|
83
|
+
config_paths = cli_arguments.get(CONFIG_PATHS_KEY, [])
|
|
84
|
+
for path in config_paths:
|
|
85
|
+
_load_config_file(path, config, sources, CONFIG_SOURCE_CLI_FILE)
|
|
86
|
+
|
|
87
|
+
# Check the command-line config options.
|
|
88
|
+
cli_configs = cli_arguments.get(CONFIGS_KEY, [])
|
|
89
|
+
for cli_config in cli_configs:
|
|
90
|
+
if ("=" not in cli_config):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Invalid configuration option '{cli_config}'."
|
|
93
|
+
+ " Configuration options must be provided in the format `<key>=<value>` when passed via the CLI."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
(key, value) = cli_config.split("=", maxsplit = 1)
|
|
97
|
+
|
|
98
|
+
key = key.strip()
|
|
99
|
+
if (key == ""):
|
|
100
|
+
raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.")
|
|
101
|
+
|
|
102
|
+
config[key] = value
|
|
103
|
+
sources[key] = ConfigSource(label = CONFIG_SOURCE_CLI)
|
|
104
|
+
|
|
105
|
+
# Finally, ignore any configs that is specified from CLI command.
|
|
106
|
+
cli_ignore_configs = cli_arguments.get(IGNORE_CONFIGS_KEY, [])
|
|
107
|
+
for ignore_config in cli_ignore_configs:
|
|
108
|
+
config.pop(ignore_config, None)
|
|
109
|
+
sources.pop(ignore_config, None)
|
|
110
|
+
|
|
111
|
+
return config, sources
|
|
112
|
+
|
|
113
|
+
def _load_config_file(
|
|
114
|
+
config_path: str,
|
|
115
|
+
config: typing.Dict[str, str],
|
|
116
|
+
sources: typing.Dict[str, ConfigSource],
|
|
117
|
+
source_label: str,
|
|
118
|
+
) -> None:
|
|
119
|
+
""" Loads config variables and the source from the given config JSON file. """
|
|
120
|
+
|
|
121
|
+
config_path = os.path.abspath(config_path)
|
|
122
|
+
for (key, value) in edq.util.json.load_path(config_path).items():
|
|
123
|
+
key = key.strip()
|
|
124
|
+
if (key == ""):
|
|
125
|
+
raise ValueError(f"Found an empty configuration option key associated with the value '{value}'.")
|
|
126
|
+
|
|
127
|
+
config[key] = value
|
|
128
|
+
sources[key] = ConfigSource(label = source_label, path = config_path)
|
|
129
|
+
|
|
130
|
+
def _get_local_config_path(
|
|
131
|
+
config_filename: str,
|
|
132
|
+
legacy_config_filename: typing.Union[str, None] = None,
|
|
133
|
+
local_config_root_cutoff: typing.Union[str, None] = None,
|
|
134
|
+
) -> typing.Union[str, None]:
|
|
135
|
+
"""
|
|
136
|
+
Search for a config file in hierarchical order.
|
|
137
|
+
Begins with the provided config file name,
|
|
138
|
+
optionally checks the legacy config file name if specified,
|
|
139
|
+
then continues up the directory tree looking for the provided config file name.
|
|
140
|
+
Returns the path to the first config file found.
|
|
141
|
+
|
|
142
|
+
If no config file is found, returns None.
|
|
143
|
+
|
|
144
|
+
The cutoff parameter limits the search depth, preventing detection of config file in higher-level directories during testing.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
# Provided config file is in current directory.
|
|
148
|
+
if (os.path.isfile(config_filename)):
|
|
149
|
+
return os.path.abspath(config_filename)
|
|
150
|
+
|
|
151
|
+
# Provided legacy config file is in current directory.
|
|
152
|
+
if (legacy_config_filename is not None):
|
|
153
|
+
if (os.path.isfile(legacy_config_filename)):
|
|
154
|
+
return os.path.abspath(legacy_config_filename)
|
|
155
|
+
|
|
156
|
+
# Provided config file is found in an ancestor directory up to the root or cutoff limit.
|
|
157
|
+
parent_dir = os.path.dirname(os.getcwd())
|
|
158
|
+
return _get_ancestor_config_file_path(
|
|
159
|
+
parent_dir,
|
|
160
|
+
config_filename = config_filename,
|
|
161
|
+
local_config_root_cutoff = local_config_root_cutoff,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _get_ancestor_config_file_path(
|
|
165
|
+
current_directory: str,
|
|
166
|
+
config_filename: str,
|
|
167
|
+
local_config_root_cutoff: typing.Union[str, None] = None,
|
|
168
|
+
) -> typing.Union[str, None]:
|
|
169
|
+
"""
|
|
170
|
+
Search through the parent directories (until root or a given cutoff directory(inclusive)) for a config file.
|
|
171
|
+
Stops at the first occurrence of the specified config file along the path to root.
|
|
172
|
+
Returns the path if a config file is found.
|
|
173
|
+
Otherwise, returns None.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
if (local_config_root_cutoff is not None):
|
|
177
|
+
local_config_root_cutoff = os.path.abspath(local_config_root_cutoff)
|
|
178
|
+
|
|
179
|
+
current_directory = os.path.abspath(current_directory)
|
|
180
|
+
for _ in range(edq.util.dirent.DEPTH_LIMIT):
|
|
181
|
+
config_file_path = os.path.join(current_directory, config_filename)
|
|
182
|
+
if (os.path.isfile(config_file_path)):
|
|
183
|
+
return config_file_path
|
|
184
|
+
|
|
185
|
+
# Check if current directory is root.
|
|
186
|
+
parent_dir = os.path.dirname(current_directory)
|
|
187
|
+
if (parent_dir == current_directory):
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
if (local_config_root_cutoff == current_directory):
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
current_directory = parent_dir
|
|
194
|
+
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def set_cli_args(parser: argparse.ArgumentParser, extra_state: typing.Dict[str, typing.Any],
|
|
198
|
+
config_filename: str = DEFAULT_CONFIG_FILENAME,
|
|
199
|
+
**kwargs: typing.Any,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Set common CLI arguments for configuration.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
parser.add_argument('--config-global', dest = GLOBAL_CONFIG_KEY,
|
|
206
|
+
action = 'store', type = str, default = get_global_config_path(config_filename),
|
|
207
|
+
help = 'Set the default global config file path (default: %(default)s).',
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
parser.add_argument('--config-file', dest = CONFIG_PATHS_KEY,
|
|
211
|
+
action = 'append', type = str, default = [],
|
|
212
|
+
help = ('Load config options from a JSON file.'
|
|
213
|
+
+ ' This flag can be specified multiple times.'
|
|
214
|
+
+ ' Files are applied in the order provided and later files override earlier ones.'
|
|
215
|
+
+ ' Will override options form both global and local config files.')
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
parser.add_argument('--config', dest = CONFIGS_KEY,
|
|
219
|
+
action = 'append', type = str, default = [],
|
|
220
|
+
help = ('Set a configuration option from the command-line.'
|
|
221
|
+
+ ' Specify options as <key>=<value> pairs.'
|
|
222
|
+
+ ' This flag can be specified multiple times.'
|
|
223
|
+
+ ' The options are applied in the order provided and later options override earlier ones.'
|
|
224
|
+
+ ' Will override options form all config files.')
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
parser.add_argument('--ignore-config-option', dest = IGNORE_CONFIGS_KEY,
|
|
228
|
+
action = 'append', type = str, default = [],
|
|
229
|
+
help = ('Ignore any config option with the specified key.'
|
|
230
|
+
+ ' The system-provided default value will be used for that option if one exists.'
|
|
231
|
+
+ ' This flag can be specified multiple times.'
|
|
232
|
+
+ ' Ignored options are processed last.')
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def load_config_into_args(
|
|
236
|
+
parser: argparse.ArgumentParser,
|
|
237
|
+
args: argparse.Namespace,
|
|
238
|
+
extra_state: typing.Dict[str, typing.Any],
|
|
239
|
+
config_filename: str = DEFAULT_CONFIG_FILENAME,
|
|
240
|
+
cli_arg_config_map: typing.Union[typing.Dict[str, str], None] = None,
|
|
241
|
+
**kwargs: typing.Any,
|
|
242
|
+
) -> None:
|
|
243
|
+
"""
|
|
244
|
+
Take in args from a parser that was passed to set_cli_args(),
|
|
245
|
+
and get the tired configuration with the appropriate parameters, and attache it to args.
|
|
246
|
+
|
|
247
|
+
Arguments that appear on the CLI as flags (e.g. `--foo bar`) can be copied over to the config options via `cli_arg_config_map`.
|
|
248
|
+
The keys of `cli_arg_config_map` represent attributes in the CLI arguments (`args`),
|
|
249
|
+
while the values represent the desired config name this argument should be set as.
|
|
250
|
+
For example, a `cli_arg_config_map` of `{'foo': 'baz'}` will make the CLI argument `--foo bar`
|
|
251
|
+
be equivalent to `--config baz=bar`.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
if (cli_arg_config_map is None):
|
|
255
|
+
cli_arg_config_map = {}
|
|
256
|
+
|
|
257
|
+
for (cli_key, config_key) in cli_arg_config_map.items():
|
|
258
|
+
value = getattr(args, cli_key, None)
|
|
259
|
+
if (value is not None):
|
|
260
|
+
getattr(args, CONFIGS_KEY).append(f"{config_key}={value}")
|
|
261
|
+
|
|
262
|
+
(config_dict, sources_dict) = get_tiered_config(
|
|
263
|
+
cli_arguments = args,
|
|
264
|
+
config_filename = config_filename,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
setattr(args, "_config", config_dict)
|
|
268
|
+
setattr(args, "_config_sources", sources_dict)
|