edq-utils 0.1.9__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.
- edq/__init__.py +5 -0
- edq/cli/__init__.py +0 -0
- 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 +49 -0
- edq/cli/version.py +28 -0
- edq/core/__init__.py +0 -0
- edq/core/argparser.py +137 -0
- edq/core/argparser_test.py +124 -0
- edq/core/config.py +268 -0
- edq/core/config_test.py +1038 -0
- edq/core/log.py +101 -0
- edq/core/version.py +6 -0
- edq/procedure/__init__.py +0 -0
- edq/procedure/verify_exchanges.py +85 -0
- edq/py.typed +0 -0
- edq/testing/__init__.py +3 -0
- edq/testing/asserts.py +65 -0
- edq/testing/cli.py +360 -0
- edq/testing/cli_test.py +15 -0
- edq/testing/httpserver.py +578 -0
- edq/testing/httpserver_test.py +424 -0
- edq/testing/run.py +142 -0
- 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/cli/tests/help_base.txt +9 -0
- edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
- edq/testing/testdata/cli/tests/version_base.txt +6 -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 +88 -0
- edq/util/__init__.py +3 -0
- edq/util/dirent.py +340 -0
- edq/util/dirent_test.py +979 -0
- edq/util/encoding.py +18 -0
- edq/util/hash.py +41 -0
- edq/util/hash_test.py +89 -0
- edq/util/json.py +180 -0
- edq/util/json_test.py +228 -0
- edq/util/net.py +1008 -0
- edq/util/parse.py +33 -0
- edq/util/pyimport.py +94 -0
- edq/util/pyimport_test.py +119 -0
- edq/util/reflection.py +32 -0
- edq/util/time.py +75 -0
- edq/util/time_test.py +107 -0
- edq_utils-0.1.9.dist-info/METADATA +164 -0
- edq_utils-0.1.9.dist-info/RECORD +83 -0
- edq_utils-0.1.9.dist-info/WHEEL +5 -0
- edq_utils-0.1.9.dist-info/licenses/LICENSE +21 -0
- edq_utils-0.1.9.dist-info/top_level.txt +1 -0
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)
|