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

Files changed (71) hide show
  1. edq/__init__.py +1 -1
  2. edq/cli/config/__init__.py +3 -0
  3. edq/cli/config/list.py +69 -0
  4. edq/cli/http/__init__.py +3 -0
  5. edq/cli/http/exchange-server.py +71 -0
  6. edq/cli/http/send-exchange.py +45 -0
  7. edq/cli/http/verify-exchanges.py +38 -0
  8. edq/cli/testing/__init__.py +3 -0
  9. edq/cli/testing/cli-test.py +12 -5
  10. edq/cli/version.py +2 -1
  11. edq/core/argparser.py +28 -3
  12. edq/core/config.py +268 -0
  13. edq/core/config_test.py +1038 -0
  14. edq/procedure/verify_exchanges.py +85 -0
  15. edq/testing/asserts.py +0 -1
  16. edq/testing/cli.py +107 -29
  17. edq/testing/cli_test.py +8 -1
  18. edq/testing/httpserver.py +553 -0
  19. edq/testing/httpserver_test.py +424 -0
  20. edq/testing/run.py +40 -10
  21. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  22. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  23. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  24. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  25. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  26. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  27. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  28. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  29. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  30. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  31. edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
  32. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  33. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  34. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  35. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  36. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  37. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  38. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  39. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  40. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  41. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  42. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  43. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  44. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  45. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  46. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  47. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  49. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  50. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  51. edq/testing/testdata/http/files/tiny.png +0 -0
  52. edq/testing/unittest.py +26 -6
  53. edq/util/dirent.py +2 -0
  54. edq/util/dirent_test.py +43 -32
  55. edq/util/json.py +21 -4
  56. edq/util/net.py +894 -0
  57. edq_utils-0.0.6.dist-info/METADATA +156 -0
  58. edq_utils-0.0.6.dist-info/RECORD +78 -0
  59. edq/util/testdata/dirent-operations/dir_1/b.txt +0 -1
  60. edq/util/testdata/dirent-operations/dir_1/dir_2/c.txt +0 -1
  61. edq/util/testdata/dirent-operations/symlink_a.txt +0 -1
  62. edq/util/testdata/dirent-operations/symlink_dir_1/b.txt +0 -1
  63. edq/util/testdata/dirent-operations/symlink_dir_1/dir_2/c.txt +0 -1
  64. edq/util/testdata/dirent-operations/symlink_file_empty +0 -0
  65. edq_utils-0.0.4.dist-info/METADATA +0 -63
  66. edq_utils-0.0.4.dist-info/RECORD +0 -41
  67. /edq/{util/testdata/dirent-operations/file_empty → procedure/__init__.py} +0 -0
  68. /edq/{util/testdata/dirent-operations → testing/testdata/http/files}/a.txt +0 -0
  69. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/WHEEL +0 -0
  70. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/licenses/LICENSE +0 -0
  71. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/top_level.txt +0 -0
@@ -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
+ }
Binary file
edq/testing/unittest.py CHANGED
@@ -14,25 +14,45 @@ class BaseTest(unittest.TestCase):
14
14
  maxDiff = None
15
15
  """ Don't limit the size of diffs. """
16
16
 
17
- def assertJSONDictEqual(self, a: typing.Dict[str, typing.Any], b: typing.Dict[str, typing.Any]) -> None: # pylint: disable=invalid-name
17
+ def assertJSONDictEqual(self, a: typing.Any, b: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name
18
18
  """
19
- Call assertDictEqual(), but supply a message containing the full JSON representation of the arguments.
19
+ Like unittest.TestCase.assertDictEqual(),
20
+ but will try to convert each comparison argument to a dict if it is not already,
21
+ and uses a default assertion message containing the full JSON representation of the arguments.
20
22
  """
21
23
 
24
+ if (not isinstance(a, dict)):
25
+ if (isinstance(a, edq.util.json.DictConverter)):
26
+ a = a.to_dict()
27
+ else:
28
+ a = vars(a)
29
+
30
+ if (not isinstance(b, dict)):
31
+ if (isinstance(b, edq.util.json.DictConverter)):
32
+ b = b.to_dict()
33
+ else:
34
+ b = vars(b)
35
+
22
36
  a_json = edq.util.json.dumps(a, indent = 4)
23
37
  b_json = edq.util.json.dumps(b, indent = 4)
24
38
 
25
- super().assertDictEqual(a, b, FORMAT_STR % (a_json, b_json))
39
+ if (message is None):
40
+ message = FORMAT_STR % (a_json, b_json)
26
41
 
27
- def assertJSONListEqual(self, a: typing.List[typing.Any], b: typing.List[typing.Any]) -> None: # pylint: disable=invalid-name
42
+ super().assertDictEqual(a, b, msg = message)
43
+
44
+ def assertJSONListEqual(self, a: typing.List[typing.Any], b: typing.List[typing.Any], message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name
28
45
  """
29
- Call assertListEqual(), but supply a message containing the full JSON representation of the arguments.
46
+ Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments.
30
47
  """
31
48
 
32
49
  a_json = edq.util.json.dumps(a, indent = 4)
33
50
  b_json = edq.util.json.dumps(b, indent = 4)
34
51
 
35
- super().assertListEqual(a, b, FORMAT_STR % (a_json, b_json))
52
+ if (message is None):
53
+ message = FORMAT_STR % (a_json, b_json)
54
+
55
+ super().assertListEqual(a, b, msg = message)
36
56
 
37
57
  def format_error_string(self, ex: typing.Union[BaseException, None]) -> str:
38
58
  """
edq/util/dirent.py CHANGED
@@ -41,6 +41,8 @@ def get_temp_path(prefix: str = '', suffix: str = '', rm: bool = True) -> str:
41
41
  while ((path is None) or exists(path)):
42
42
  path = os.path.join(tempfile.gettempdir(), prefix + str(uuid.uuid4()) + suffix)
43
43
 
44
+ path = os.path.realpath(path)
45
+
44
46
  if (rm):
45
47
  atexit.register(remove, path)
46
48
 
edq/util/dirent_test.py CHANGED
@@ -3,32 +3,51 @@ import os
3
3
  import edq.testing.unittest
4
4
  import edq.util.dirent
5
5
 
6
- THIS_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
7
- TEST_BASE_DIR = os.path.join(THIS_DIR, 'testdata', 'dirent-operations')
8
- """
9
- This test data directory is laid out as:
10
- .
11
- ├── a.txt
12
- ├── dir_1
13
- │   ├── b.txt
14
- │   └── dir_2
15
- │   └── c.txt
16
- ├── dir_empty
17
- ├── file_empty
18
- ├── symlink_a.txt -> a.txt
19
- ├── symlink_dir_1 -> dir_1
20
- ├── symlink_dir_empty -> dir_empty
21
- └── symlink_file_empty -> file_empty
22
-
23
- Where non-empty files are filled with their filename (without the extension).
24
- dir_empty will not exist in the repository (since it is an empty directory),
25
- but it will be created by _prep_temp_dir().
26
- """
27
-
28
6
  DIRENT_TYPE_DIR = 'dir'
29
7
  DIRENT_TYPE_FILE = 'file'
30
8
  DIRENT_TYPE_BROKEN_SYMLINK = 'broken_symlink'
31
9
 
10
+ def create_test_dir(temp_dir_prefix: str) -> str:
11
+ """
12
+ Create a temp dir and populate it with dirents for testing.
13
+
14
+ This test data directory is laid out as:
15
+ .
16
+ ├── a.txt
17
+ ├── dir_1
18
+ │   ├── b.txt
19
+ │   └── dir_2
20
+ │   └── c.txt
21
+ ├── dir_empty
22
+ ├── file_empty
23
+ ├── symlink_a.txt -> a.txt
24
+ ├── symlink_dir_1 -> dir_1
25
+ ├── symlink_dir_empty -> dir_empty
26
+ └── symlink_file_empty -> file_empty
27
+
28
+ Where non-empty files are filled with their filename (without the extension).
29
+ """
30
+
31
+ temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix)
32
+
33
+ # Dirs
34
+ edq.util.dirent.mkdir(os.path.join(temp_dir, 'dir_1', 'dir_2'))
35
+ edq.util.dirent.mkdir(os.path.join(temp_dir, 'dir_empty'))
36
+
37
+ # Files
38
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'a.txt'), 'a')
39
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'file_empty'), '')
40
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'dir_1', 'b.txt'), 'b')
41
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'dir_1', 'dir_2', 'c.txt'), 'c')
42
+
43
+ # Links
44
+ os.symlink('a.txt', os.path.join(temp_dir, 'symlink_a.txt'))
45
+ os.symlink('dir_1', os.path.join(temp_dir, 'symlink_dir_1'))
46
+ os.symlink('dir_empty', os.path.join(temp_dir, 'symlink_dir_empty'))
47
+ os.symlink('file_empty', os.path.join(temp_dir, 'symlink_file_empty'))
48
+
49
+ return temp_dir
50
+
32
51
  class TestDirent(edq.testing.unittest.BaseTest):
33
52
  """ Test basic operations on dirents. """
34
53
 
@@ -363,10 +382,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
363
382
  self.assertEqual(expected_contents, actual_contents)
364
383
 
365
384
  def test_copy_contents_base(self):
366
- """
367
- Test copying the contents of a dirent.
368
- Note that the base functionality of copy_contents() is tested by test_setup().
369
- """
385
+ """ Test copying the contents of a dirent. """
370
386
 
371
387
  # [(source, dest, no clobber?, error substring), ...]
372
388
  test_cases = [
@@ -877,12 +893,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
877
893
  self._check_existing_paths(temp_dir, expected_paths)
878
894
 
879
895
  def _prep_temp_dir(self):
880
- temp_dir = edq.util.dirent.get_temp_dir(prefix = 'edq_test_dirent_')
881
-
882
- edq.util.dirent.mkdir(os.path.join(temp_dir, 'dir_empty'))
883
- edq.util.dirent.copy_contents(TEST_BASE_DIR, temp_dir)
884
-
885
- return temp_dir
896
+ return create_test_dir('edq_test_dirent_')
886
897
 
887
898
  def _check_existing_paths(self, base_dir, raw_paths):
888
899
  """
edq/util/json.py CHANGED
@@ -4,37 +4,45 @@ Specifically, we try to be flexible when reading (using JSON5),
4
4
  and strict when writing (using vanilla JSON).
5
5
  """
6
6
 
7
- import abc
8
7
  import enum
9
8
  import json
9
+ import os
10
10
  import typing
11
11
 
12
12
  import json5
13
13
 
14
14
  import edq.util.dirent
15
15
 
16
- class DictConverter(abc.ABC):
16
+ class DictConverter():
17
17
  """
18
18
  A base class for class that can represent (serialize) and reconstruct (deserialize) themselves as/from a dict.
19
19
  The intention is that the dict can then be cleanly converted to/from JSON.
20
+
21
+ General (but inefficient) implementations of several core Python equality, comparison, and representation methods are provided.
20
22
  """
21
23
 
22
- @abc.abstractmethod
23
24
  def to_dict(self) -> typing.Dict[str, typing.Any]:
24
25
  """
25
26
  Return a dict that can be used to represent this object.
26
27
  If the dict is passed to from_dict(), an identical object should be reconstructed.
28
+
29
+ A general (but inefficient) implementation is provided by default.
27
30
  """
28
31
 
32
+ return vars(self).copy()
33
+
29
34
  @classmethod
30
- @abc.abstractmethod
31
35
  # Note that `typing.Self` is returned, but that is introduced in Python 3.12.
32
36
  def from_dict(cls, data: typing.Dict[str, typing.Any]) -> typing.Any:
33
37
  """
34
38
  Return an instance of this subclass created using the given dict.
35
39
  If the dict came from to_dict(), the returned object should be identical to the original.
40
+
41
+ A general (but inefficient) implementation is provided by default.
36
42
  """
37
43
 
44
+ return cls(**data)
45
+
38
46
  def __eq__(self, other: object) -> bool:
39
47
  """
40
48
  Check for equality.
@@ -49,6 +57,12 @@ class DictConverter(abc.ABC):
49
57
 
50
58
  return bool(self.to_dict() == other.to_dict()) # type: ignore[attr-defined]
51
59
 
60
+ def __lt__(self, other: 'DictConverter') -> bool:
61
+ return dumps(self) < dumps(other)
62
+
63
+ def __hash__(self) -> int:
64
+ return hash(dumps(self))
65
+
52
66
  def __str__(self) -> str:
53
67
  return dumps(self)
54
68
 
@@ -107,6 +121,9 @@ def load_path(
107
121
  otherwise use JSON5.
108
122
  """
109
123
 
124
+ if (os.path.isdir(path)):
125
+ raise IsADirectoryError(f"Cannot open JSON file, expected a file but got a directory at '{path}'.")
126
+
110
127
  try:
111
128
  with open(path, 'r', encoding = encoding) as file:
112
129
  return load(file, strict = strict, **kwargs)