edq-utils 0.2.3__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.

Files changed (88) hide show
  1. edq/__init__.py +5 -0
  2. edq/cli/__init__.py +0 -0
  3. edq/cli/__main__.py +17 -0
  4. edq/cli/config/__init__.py +3 -0
  5. edq/cli/config/__main__.py +15 -0
  6. edq/cli/config/list.py +69 -0
  7. edq/cli/http/__init__.py +3 -0
  8. edq/cli/http/__main__.py +15 -0
  9. edq/cli/http/exchange-server.py +71 -0
  10. edq/cli/http/send-exchange.py +45 -0
  11. edq/cli/http/verify-exchanges.py +38 -0
  12. edq/cli/testing/__init__.py +3 -0
  13. edq/cli/testing/__main__.py +15 -0
  14. edq/cli/testing/cli-test.py +49 -0
  15. edq/cli/version.py +28 -0
  16. edq/core/__init__.py +0 -0
  17. edq/core/argparser.py +137 -0
  18. edq/core/argparser_test.py +124 -0
  19. edq/core/config.py +268 -0
  20. edq/core/config_test.py +1038 -0
  21. edq/core/log.py +101 -0
  22. edq/core/version.py +6 -0
  23. edq/procedure/__init__.py +0 -0
  24. edq/procedure/verify_exchanges.py +85 -0
  25. edq/py.typed +0 -0
  26. edq/testing/__init__.py +3 -0
  27. edq/testing/asserts.py +65 -0
  28. edq/testing/cli.py +360 -0
  29. edq/testing/cli_test.py +15 -0
  30. edq/testing/httpserver.py +578 -0
  31. edq/testing/httpserver_test.py +424 -0
  32. edq/testing/run.py +142 -0
  33. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  34. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  35. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  36. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  37. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  38. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  39. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  40. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  41. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  42. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  43. edq/testing/testdata/cli/tests/help_base.txt +9 -0
  44. edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
  45. edq/testing/testdata/cli/tests/version_base.txt +6 -0
  46. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  47. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  49. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  50. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  51. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  52. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  53. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  54. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  55. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  56. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  57. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  58. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  59. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  60. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  61. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  62. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  63. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  64. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  65. edq/testing/testdata/http/files/a.txt +1 -0
  66. edq/testing/testdata/http/files/tiny.png +0 -0
  67. edq/testing/unittest.py +88 -0
  68. edq/util/__init__.py +3 -0
  69. edq/util/cli.py +151 -0
  70. edq/util/dirent.py +346 -0
  71. edq/util/dirent_test.py +1004 -0
  72. edq/util/encoding.py +18 -0
  73. edq/util/hash.py +41 -0
  74. edq/util/hash_test.py +89 -0
  75. edq/util/json.py +180 -0
  76. edq/util/json_test.py +228 -0
  77. edq/util/net.py +1047 -0
  78. edq/util/parse.py +33 -0
  79. edq/util/pyimport.py +94 -0
  80. edq/util/pyimport_test.py +119 -0
  81. edq/util/reflection.py +32 -0
  82. edq/util/time.py +75 -0
  83. edq/util/time_test.py +107 -0
  84. edq_utils-0.2.3.dist-info/METADATA +164 -0
  85. edq_utils-0.2.3.dist-info/RECORD +88 -0
  86. edq_utils-0.2.3.dist-info/WHEEL +5 -0
  87. edq_utils-0.2.3.dist-info/licenses/LICENSE +21 -0
  88. edq_utils-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple#a",
4
+ "response_body": "simple_anchor"
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "files": [
5
+ {
6
+ "path": "../files/a.txt"
7
+ }
8
+ ],
9
+ "response_body": "simple_file"
10
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "files": [
5
+ {
6
+ "path": "../files/tiny.png"
7
+ }
8
+ ],
9
+ "response_body": "simple_file_binary"
10
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "parameters": {
5
+ "a": "1",
6
+ "b": "2"
7
+ },
8
+ "files": [
9
+ {
10
+ "path": "../files/a.txt"
11
+ }
12
+ ],
13
+ "response_body": "simple_file_get_params"
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "files": [
5
+ {
6
+ "path": "../files/a.txt"
7
+ },
8
+ {
9
+ "path": "../files/tiny.png"
10
+ }
11
+ ],
12
+ "response_body": "simple_file_multiple"
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "files": [
5
+ {
6
+ "path": "../files/a.txt",
7
+ "name": "foo.txt"
8
+ }
9
+ ],
10
+ "response_body": "simple_file_name"
11
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "method": "POST",
3
+ "url": "simple",
4
+ "files": [
5
+ {
6
+ "path": "../files/a.txt"
7
+ },
8
+ {
9
+ "path": "../files/tiny.png"
10
+ }
11
+ ],
12
+ "response_body": "simple_file_post_multiple"
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "method": "POST",
3
+ "url": "simple",
4
+ "parameters": {
5
+ "a": "1",
6
+ "b": "2"
7
+ },
8
+ "files": [
9
+ {
10
+ "path": "../files/a.txt"
11
+ }
12
+ ],
13
+ "response_body": "simple_file_post_params"
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "headers": {
5
+ "a": "1"
6
+ },
7
+ "response_body": "simple_headers"
8
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple/jsonresponse/dict",
4
+ "response_body": {
5
+ "a": 1
6
+ }
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple/jsonresponse/list",
4
+ "response_body": [
5
+ {
6
+ "a": 1
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "parameters": {
5
+ "a": "1",
6
+ "b": "2"
7
+ },
8
+ "response_body": "simple_params"
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "method": "POST",
3
+ "url": "simple",
4
+ "response_body": "simple_post"
5
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "method": "POST",
3
+ "url": "simple",
4
+ "parameters": {
5
+ "a": "1",
6
+ "b": "2"
7
+ },
8
+ "response_body": "simple_post_params"
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "method": "POST",
3
+ "url": "simple?c=3&d=4",
4
+ "response_body": "simple_post_urlparams"
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple?c=3&d=4",
4
+ "response_body": "simple_urlparams"
5
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple",
4
+ "parameters": {
5
+ "a": ["1", "2"]
6
+ },
7
+ "response_body": "specialcase_listparams_explicit"
8
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "method": "GET",
3
+ "url": "simple?b=1&b=2",
4
+ "response_body": "specialcase_listparams_url"
5
+ }
@@ -0,0 +1 @@
1
+ a
Binary file
@@ -0,0 +1,88 @@
1
+ import typing
2
+ import unittest
3
+
4
+ import edq.util.json
5
+ import edq.util.reflection
6
+
7
+ FORMAT_STR: str = "\n--- Expected ---\n%s\n--- Actual ---\n%s\n---\n"
8
+
9
+ class BaseTest(unittest.TestCase):
10
+ """
11
+ A base class for unit tests.
12
+ """
13
+
14
+ maxDiff = None
15
+ """ Don't limit the size of diffs. """
16
+
17
+ def assertJSONEqual(self, a: typing.Any, b: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name
18
+ """
19
+ Like unittest.TestCase.assertEqual(),
20
+ but uses a default assertion message containing the full JSON representation of the arguments.
21
+ """
22
+
23
+ a_json = edq.util.json.dumps(a, indent = 4)
24
+ b_json = edq.util.json.dumps(b, indent = 4)
25
+
26
+ if (message is None):
27
+ message = FORMAT_STR % (a_json, b_json)
28
+
29
+ super().assertEqual(a, b, msg = message)
30
+
31
+ def assertJSONDictEqual(self, a: typing.Any, b: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name
32
+ """
33
+ Like unittest.TestCase.assertDictEqual(),
34
+ but will try to convert each comparison argument to a dict if it is not already,
35
+ and uses a default assertion message containing the full JSON representation of the arguments.
36
+ """
37
+
38
+ if (not isinstance(a, dict)):
39
+ if (isinstance(a, edq.util.json.DictConverter)):
40
+ a = a.to_dict()
41
+ else:
42
+ a = vars(a)
43
+
44
+ if (not isinstance(b, dict)):
45
+ if (isinstance(b, edq.util.json.DictConverter)):
46
+ b = b.to_dict()
47
+ else:
48
+ b = vars(b)
49
+
50
+ a_json = edq.util.json.dumps(a, indent = 4)
51
+ b_json = edq.util.json.dumps(b, indent = 4)
52
+
53
+ if (message is None):
54
+ message = FORMAT_STR % (a_json, b_json)
55
+
56
+ super().assertDictEqual(a, b, msg = message)
57
+
58
+ def assertJSONListEqual(self, a: typing.List[typing.Any], b: typing.List[typing.Any], message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name
59
+ """
60
+ Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments.
61
+ """
62
+
63
+ a_json = edq.util.json.dumps(a, indent = 4)
64
+ b_json = edq.util.json.dumps(b, indent = 4)
65
+
66
+ if (message is None):
67
+ message = FORMAT_STR % (a_json, b_json)
68
+
69
+ super().assertListEqual(a, b, msg = message)
70
+
71
+ def format_error_string(self, ex: typing.Union[BaseException, None]) -> str:
72
+ """
73
+ Format an error string from an exception so it can be checked for testing.
74
+ The type of the error will be included,
75
+ and any nested errors will be joined together.
76
+ """
77
+
78
+ parts = []
79
+
80
+ while (ex is not None):
81
+ type_name = edq.util.reflection.get_qualified_name(ex)
82
+ message = str(ex)
83
+
84
+ parts.append(f"{type_name}: {message}")
85
+
86
+ ex = ex.__cause__
87
+
88
+ return "; ".join(parts)
edq/util/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ Low-level utilities.
3
+ """
edq/util/cli.py ADDED
@@ -0,0 +1,151 @@
1
+ """
2
+ Show the CLI tools available in this package.
3
+
4
+ This will look for objects that "look like" CLI tools.
5
+ A package looks like a CLI package if it has a __main__.py file.
6
+ A module looks like a CLI tool if it has the following functions:
7
+ - `def _get_parser() -> argparse.ArgumentParser:`
8
+ - `def run_cli(args: argparse.Namespace) -> int:`
9
+ """
10
+
11
+ import argparse
12
+ import inspect
13
+ import os
14
+
15
+ import edq.util.dirent
16
+ import edq.util.pyimport
17
+
18
+ def auto_list(
19
+ recursive: bool = False,
20
+ skip_dirs: bool = False,
21
+ ) -> None:
22
+ """
23
+ Print the caller's docstring and call _list_dir() on it,
24
+ but will figure out the package's docstring, base_dir, and command_prefix automatically.
25
+ This will use the inspect library, so only use in places that use code normally.
26
+ The first stack frame not in this file will be used.
27
+ """
28
+
29
+ this_path = os.path.realpath(__file__)
30
+
31
+ caller_frame_info = None
32
+ for frame_info in inspect.stack():
33
+ if (edq.util.dirent.same(this_path, frame_info.filename)):
34
+ # Ignore this file.
35
+ continue
36
+
37
+ caller_frame_info = frame_info
38
+ break
39
+
40
+ if (caller_frame_info is None):
41
+ raise ValueError("Unable to determine caller's stack frame.")
42
+
43
+ path = caller_frame_info.filename
44
+ base_dir = os.path.dirname(path)
45
+
46
+ try:
47
+ module = inspect.getmodule(caller_frame_info.frame)
48
+ if (module is None):
49
+ raise ValueError(f"Unable to get module for '{path}'.")
50
+ except Exception as ex:
51
+ raise ValueError("Unable to get caller information for listing CLI information.") from ex
52
+
53
+ if (module.__package__ is None):
54
+ raise ValueError(f"Caller module has no package information: '{path}'.")
55
+
56
+ if (module.__doc__ is None):
57
+ raise ValueError(f"Caller module has no docstring: '{path}'.")
58
+
59
+ print(module.__doc__.strip())
60
+ _list_dir(base_dir, module.__package__, recursive, skip_dirs)
61
+
62
+ def _list_dir(base_dir: str, command_prefix: str, recursive: bool, skip_dirs: bool) -> None:
63
+ """ List/descend the given dir. """
64
+
65
+ for dirent in sorted(os.listdir(base_dir)):
66
+ path = os.path.join(base_dir, dirent)
67
+ cmd = command_prefix + '.' + os.path.splitext(dirent)[0]
68
+
69
+ if (dirent.startswith('__')):
70
+ continue
71
+
72
+ if (os.path.isfile(path)):
73
+ _handle_file(path, cmd)
74
+ else:
75
+ if (not skip_dirs):
76
+ _handle_dir(path, cmd)
77
+
78
+ if (recursive):
79
+ _list_dir(path, cmd, recursive, skip_dirs)
80
+
81
+ def _handle_file(path: str, cmd: str) -> None:
82
+ """ Process a file (possible module). """
83
+
84
+ if (not path.endswith('.py')):
85
+ return
86
+
87
+ try:
88
+ module = edq.util.pyimport.import_path(path)
89
+ except Exception:
90
+ print("ERROR Importing: ", path)
91
+ return
92
+
93
+ if ('_get_parser' not in dir(module)):
94
+ return
95
+
96
+ parser = module._get_parser()
97
+ parser.prog = 'python3 -m ' + cmd
98
+
99
+ print()
100
+ print(cmd)
101
+ print(parser.description)
102
+ parser.print_usage()
103
+
104
+ def _handle_dir(path: str, cmd: str) -> None:
105
+ """ Process a dir (possible package). """
106
+
107
+ try:
108
+ module = edq.util.pyimport.import_path(os.path.join(path, '__main__.py'))
109
+ except Exception:
110
+ return
111
+
112
+ description = module.__doc__.strip()
113
+
114
+ print()
115
+ print(cmd + '.*')
116
+ print(description)
117
+ print(f"See `python3 -m {cmd}` for more information.")
118
+
119
+ def _get_parser() -> argparse.ArgumentParser:
120
+ parser = argparse.ArgumentParser(
121
+ description = __doc__.strip(),
122
+ epilog = ("Note that you don't need to provide a package as an argument,"
123
+ + " since you already called this on the target package."))
124
+
125
+ parser.add_argument('-r', '--recursive', dest = 'recursive',
126
+ action = 'store_true', default = False,
127
+ help = 'Recur into each package to look for tools and subpackages (default: %(default)s).')
128
+
129
+ parser.add_argument('-s', '--skip-dirs', dest = 'skip_dirs',
130
+ action = 'store_true', default = False,
131
+ help = ('Do not output information about directories/packages,'
132
+ + ' only tools/files/modules (default: %(default)s).'))
133
+
134
+ return parser
135
+
136
+ def run_cli(args: argparse.Namespace) -> int:
137
+ """
138
+ List the caller's dir.
139
+ """
140
+
141
+ auto_list(recursive = args.recursive, skip_dirs = args.skip_dirs)
142
+
143
+ return 0
144
+
145
+ def main() -> int:
146
+ """
147
+ Run as if this process has been called as a executable.
148
+ This will parse the command line and list the caller's dir.
149
+ """
150
+
151
+ return run_cli(_get_parser().parse_args())