edq-utils 0.1.2__tar.gz → 0.1.4__tar.gz

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.
Files changed (104) hide show
  1. {edq_utils-0.1.2 → edq_utils-0.1.4}/PKG-INFO +1 -1
  2. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/__init__.py +1 -1
  3. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/cli.py +33 -5
  4. edq_utils-0.1.4/edq/util/hash.py +38 -0
  5. edq_utils-0.1.4/edq/util/hash_test.py +89 -0
  6. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/net.py +8 -1
  7. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq_utils.egg-info/PKG-INFO +1 -1
  8. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq_utils.egg-info/SOURCES.txt +2 -0
  9. {edq_utils-0.1.2 → edq_utils-0.1.4}/.github/workflows/main.yml +0 -0
  10. {edq_utils-0.1.2 → edq_utils-0.1.4}/.gitignore +0 -0
  11. {edq_utils-0.1.2 → edq_utils-0.1.4}/.mypy.ini +0 -0
  12. {edq_utils-0.1.2 → edq_utils-0.1.4}/.pylintrc +0 -0
  13. {edq_utils-0.1.2 → edq_utils-0.1.4}/LICENSE +0 -0
  14. {edq_utils-0.1.2 → edq_utils-0.1.4}/README.md +0 -0
  15. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/__init__.py +0 -0
  16. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/config/__init__.py +0 -0
  17. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/config/list.py +0 -0
  18. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/http/__init__.py +0 -0
  19. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/http/exchange-server.py +0 -0
  20. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/http/send-exchange.py +0 -0
  21. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/http/verify-exchanges.py +0 -0
  22. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/testing/__init__.py +0 -0
  23. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/testing/cli-test.py +0 -0
  24. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/cli/version.py +0 -0
  25. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/__init__.py +0 -0
  26. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/argparser.py +0 -0
  27. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/argparser_test.py +0 -0
  28. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/config.py +0 -0
  29. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/config_test.py +0 -0
  30. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/log.py +0 -0
  31. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/core/version.py +0 -0
  32. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/procedure/__init__.py +0 -0
  33. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/procedure/verify_exchanges.py +0 -0
  34. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/py.typed +0 -0
  35. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/__init__.py +0 -0
  36. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/asserts.py +0 -0
  37. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/cli_test.py +0 -0
  38. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/httpserver.py +0 -0
  39. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/httpserver_test.py +0 -0
  40. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/run.py +0 -0
  41. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/data/configs/empty/edq-config.json +0 -0
  42. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +0 -0
  43. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +0 -0
  44. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/data/configs/value-number/edq-config.json +0 -0
  45. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_base.txt +0 -0
  46. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +0 -0
  47. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +0 -0
  48. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +0 -0
  49. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +0 -0
  50. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +0 -0
  51. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/help_base.txt +0 -0
  52. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/platform_skip.txt +0 -0
  53. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/cli/tests/version_base.txt +0 -0
  54. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple.httpex.json +0 -0
  55. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +0 -0
  56. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file.httpex.json +0 -0
  57. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +0 -0
  58. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +0 -0
  59. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +0 -0
  60. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +0 -0
  61. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +0 -0
  62. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +0 -0
  63. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_headers.httpex.json +0 -0
  64. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +0 -0
  65. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +0 -0
  66. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_params.httpex.json +0 -0
  67. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_post.httpex.json +0 -0
  68. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +0 -0
  69. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +0 -0
  70. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +0 -0
  71. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +0 -0
  72. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +0 -0
  73. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/files/a.txt +0 -0
  74. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/testdata/http/files/tiny.png +0 -0
  75. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/testing/unittest.py +0 -0
  76. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/__init__.py +0 -0
  77. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/dirent.py +0 -0
  78. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/dirent_test.py +0 -0
  79. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/json.py +0 -0
  80. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/json_test.py +0 -0
  81. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/pyimport.py +0 -0
  82. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/pyimport_test.py +0 -0
  83. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/reflection.py +0 -0
  84. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/time.py +0 -0
  85. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq/util/time_test.py +0 -0
  86. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq_utils.egg-info/dependency_links.txt +0 -0
  87. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq_utils.egg-info/requires.txt +0 -0
  88. {edq_utils-0.1.2 → edq_utils-0.1.4}/edq_utils.egg-info/top_level.txt +0 -0
  89. {edq_utils-0.1.2 → edq_utils-0.1.4}/pyproject.toml +0 -0
  90. {edq_utils-0.1.2 → edq_utils-0.1.4}/requirements-dev.txt +0 -0
  91. {edq_utils-0.1.2 → edq_utils-0.1.4}/requirements.txt +0 -0
  92. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/build_site.sh +0 -0
  93. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/check_all.sh +0 -0
  94. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/check_lint.sh +0 -0
  95. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/check_python_version.sh +0 -0
  96. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/check_strict_types.sh +0 -0
  97. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/check_types.sh +0 -0
  98. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/gen_docs.sh +0 -0
  99. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/run_tests.sh +0 -0
  100. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/template/html/css/style.css +0 -0
  101. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/template/html/favicon.ico +0 -0
  102. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/template/html/images/favicon.png +0 -0
  103. {edq_utils-0.1.2 → edq_utils-0.1.4}/scripts/template/html/index.html +0 -0
  104. {edq_utils-0.1.2 → edq_utils-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edq-utils
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Common utilities used by EduLinq Python projects.
5
5
  Author-email: Eriq Augustine <eriq@edulinq.org>
6
6
  License: MIT License
@@ -2,4 +2,4 @@
2
2
  General Python tools used by several EduLinq projects.
3
3
  """
4
4
 
5
- __version__ = '0.1.2'
5
+ __version__ = '0.1.4'
@@ -214,6 +214,22 @@ class CLITestInfo:
214
214
 
215
215
  return CLITestInfo(test_name, base_dir, data_dir, temp_dir, **options)
216
216
 
217
+ @typing.runtime_checkable
218
+ class TestMethodWrapperFunction(typing.Protocol):
219
+ """
220
+ A function that can be used to wrap/modify a CLI test method before it is attached to the test class.
221
+ """
222
+
223
+ def __call__(self,
224
+ test_method: typing.Callable,
225
+ test_info_path: str,
226
+ ) -> typing.Callable:
227
+ """
228
+ Wrap and/or modify the CLI test method before it is attached to the test class.
229
+ See _get_test_method() for the input method.
230
+ The returned method will be used in-place of the input one.
231
+ """
232
+
217
233
  def read_test_file(path: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
218
234
  """ Read a test case file and split the output into JSON data and text. """
219
235
 
@@ -257,7 +273,9 @@ def replace_path_pattern(text: str, key: str, target_dir: str) -> str:
257
273
  def _get_test_method(test_name: str, path: str, data_dir: str) -> typing.Callable:
258
274
  """ Get a test method that represents the test case at the given path. """
259
275
 
260
- def __method(self: edq.testing.unittest.BaseTest) -> None:
276
+ def __method(self: edq.testing.unittest.BaseTest,
277
+ reraise_exception_types: typing.Union[typing.Tuple[typing.Type], None] = None,
278
+ **kwargs: typing.Any) -> None:
261
279
  test_info = CLITestInfo.load_path(path, test_name, getattr(self, BASE_TEMP_DIR_ATTR), data_dir)
262
280
 
263
281
  # Allow the test class a chance to modify the test info before the test runs.
@@ -281,6 +299,9 @@ def _get_test_method(test_name: str, path: str, data_dir: str) -> typing.Callabl
281
299
  if (test_info.error):
282
300
  self.fail(f"No error was not raised when one was expected ('{str(test_info.expected_stdout)}').")
283
301
  except BaseException as ex:
302
+ if ((reraise_exception_types is not None) and isinstance(ex, reraise_exception_types)):
303
+ raise ex
304
+
284
305
  if (not test_info.error):
285
306
  raise ex
286
307
 
@@ -306,7 +327,8 @@ def _get_test_method(test_name: str, path: str, data_dir: str) -> typing.Callabl
306
327
 
307
328
  return __method
308
329
 
309
- def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str]) -> None:
330
+ def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str],
331
+ test_method_wrapper: typing.Union[TestMethodWrapperFunction, None] = None) -> None:
310
332
  """ Add tests from the given test files. """
311
333
 
312
334
  # Attach a temp directory to the testing class so all tests can share a common base temp dir.
@@ -321,12 +343,18 @@ def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str]) -
321
343
  test_name = 'test_cli__' + basename
322
344
 
323
345
  try:
324
- setattr(target_class, test_name, _get_test_method(test_name, path, data_dir))
346
+ test_method = _get_test_method(test_name, path, data_dir)
325
347
  except Exception as ex:
326
348
  raise ValueError(f"Failed to parse test case '{path}'.") from ex
327
349
 
328
- def discover_test_cases(target_class: type, test_cases_dir: str, data_dir: str) -> None:
350
+ if (test_method_wrapper is not None):
351
+ test_method = test_method_wrapper(test_method, path)
352
+
353
+ setattr(target_class, test_name, test_method)
354
+
355
+ def discover_test_cases(target_class: type, test_cases_dir: str, data_dir: str,
356
+ test_method_wrapper: typing.Union[TestMethodWrapperFunction, None] = None) -> None:
329
357
  """ Look in the text cases directory for any test cases and add them as test methods to the test class. """
330
358
 
331
359
  paths = list(sorted(glob.glob(os.path.join(test_cases_dir, "**", "*.txt"), recursive = True)))
332
- add_test_paths(target_class, data_dir, paths)
360
+ add_test_paths(target_class, data_dir, paths, test_method_wrapper = test_method_wrapper)
@@ -0,0 +1,38 @@
1
+ import hashlib
2
+ import typing
3
+
4
+ import edq.util.dirent
5
+
6
+ DEFAULT_CLIP_HASH_LENGTH: int = 8
7
+
8
+ def sha256_hex(payload: typing.Any, encoding: str = edq.util.dirent.DEFAULT_ENCODING) -> str:
9
+ """ Compute and return the hex string of the SHA3-256 encoding of the payload. """
10
+
11
+ digest = hashlib.new('sha256')
12
+ digest.update(payload.encode(encoding))
13
+ return digest.hexdigest()
14
+
15
+ def clip_text(text: str, max_length: int, hash_length: int = DEFAULT_CLIP_HASH_LENGTH) -> str:
16
+ """
17
+ Return a clipped version of the input text that is no longer than the specified length.
18
+ If the base text is found to be too long,
19
+ then enough if the tail of the text will be removed to insert a note about the clipping
20
+ and the first |hash_length| characters of the hash from sha256_hex().
21
+
22
+ Note that the max length is actually a soft cap.
23
+ Longer strings can be generated if the original text is shorter than the notification
24
+ that will be inserted into the clipped text.
25
+ """
26
+
27
+ if (len(text) <= max_length):
28
+ return text
29
+
30
+ hash_hex = sha256_hex(text)
31
+ notification = f"[text clipped {hash_hex[0:hash_length]}]"
32
+
33
+ # Don't clip the text if the final string would be longer.
34
+ if (len(notification) >= len(text)):
35
+ return text
36
+
37
+ keep_length = max(0, max_length - len(notification))
38
+ return text[0:keep_length] + notification
@@ -0,0 +1,89 @@
1
+ import edq.testing.unittest
2
+ import edq.util.hash
3
+
4
+ class TestHash(edq.testing.unittest.BaseTest):
5
+ """ Test hash-based operations. """
6
+
7
+ def test_sha256_hex_base(self):
8
+ """ Test the base sha256 hash. """
9
+
10
+ # [(input, expected), ...]
11
+ test_cases = [
12
+ ('foo', '2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'),
13
+ ('abcdefghijklmnopqrstuvwxyz1234567890', '77d721c817f9d216c1fb783bcad9cdc20aaa2427402683f1f75dd6dfbe657470'),
14
+ ('', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'),
15
+ ]
16
+
17
+ for (i, test_case) in enumerate(test_cases):
18
+ (text, expected) = test_case
19
+
20
+ with self.subTest(msg = f"Case {i} ('{text}'):"):
21
+ actual = edq.util.hash.sha256_hex(text)
22
+ self.assertEqual(expected, actual)
23
+
24
+ def test_clip_text_base(self):
25
+ """ Test the base functionality of clip_text(). """
26
+
27
+ # [(text, max length, kwargs, expected), ...]
28
+ test_cases = [
29
+ # No Clip
30
+ (
31
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
32
+ 100,
33
+ {},
34
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
35
+ ),
36
+
37
+ # Base Clip
38
+ (
39
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
40
+ 30,
41
+ {},
42
+ 'abcdefg[text clipped 77d721c8]',
43
+ ),
44
+
45
+ # Full Clip
46
+ (
47
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
48
+ 23,
49
+ {},
50
+ '[text clipped 77d721c8]',
51
+ ),
52
+
53
+ # Over Clip
54
+ (
55
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
56
+ 10,
57
+ {},
58
+ '[text clipped 77d721c8]',
59
+ ),
60
+
61
+ # Different Hash Length
62
+ (
63
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
64
+ 30,
65
+ {'hash_length': 10},
66
+ 'abcde[text clipped 77d721c817]',
67
+ ),
68
+
69
+ # Notification Longer Than Text
70
+ (
71
+ 'abc',
72
+ 1,
73
+ {},
74
+ 'abc',
75
+ ),
76
+ (
77
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
78
+ 10,
79
+ {'hash_length': 64},
80
+ 'abcdefghijklmnopqrstuvwxyz1234567890',
81
+ ),
82
+ ]
83
+
84
+ for (i, test_case) in enumerate(test_cases):
85
+ (text, max_length, kwargs, expected) = test_case
86
+
87
+ with self.subTest(msg = f"Case {i} ('{text}'):"):
88
+ actual = edq.util.hash.clip_text(text, max_length, **kwargs)
89
+ self.assertEqual(expected, actual)
@@ -19,6 +19,7 @@ import requests
19
19
  import requests_toolbelt.multipart.decoder
20
20
 
21
21
  import edq.util.dirent
22
+ import edq.util.hash
22
23
  import edq.util.json
23
24
  import edq.util.pyimport
24
25
 
@@ -30,6 +31,9 @@ DEFAULT_REQUEST_TIMEOUT_SECS: float = 10.0
30
31
 
31
32
  DEFAULT_HTTP_EXCHANGE_EXTENSION: str= '.httpex.json'
32
33
 
34
+ QUERY_CLIP_LENGTH: int = 100
35
+ """ If the query portion of an HTTPExhange being saved is longer than this, then clip the name. """
36
+
33
37
  ANCHOR_HEADER_KEY: str = 'edq-anchor'
34
38
  """
35
39
  By default, requests made via make_request() will send a header with this key that includes the anchor component of the URL.
@@ -729,8 +733,11 @@ def make_request(method: str, url: str,
729
733
 
730
734
  query = urllib.parse.urlencode(exchange.parameters)
731
735
  if (query != ''):
736
+ # The query can get very long, so we may have to clip it.
737
+ query_text = edq.util.hash.clip_text(query, QUERY_CLIP_LENGTH)
738
+
732
739
  # Note that the '?' is URL encoded.
733
- path += f"%3F{query}"
740
+ path += f"%3F{query_text}"
734
741
 
735
742
  path += f"_{method}{http_exchange_extension}"
736
743
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edq-utils
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Common utilities used by EduLinq Python projects.
5
5
  Author-email: Eriq Augustine <eriq@edulinq.org>
6
6
  License: MIT License
@@ -73,6 +73,8 @@ edq/testing/testdata/http/files/tiny.png
73
73
  edq/util/__init__.py
74
74
  edq/util/dirent.py
75
75
  edq/util/dirent_test.py
76
+ edq/util/hash.py
77
+ edq/util/hash_test.py
76
78
  edq/util/json.py
77
79
  edq/util/json_test.py
78
80
  edq/util/net.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes