httpdbg 0.28.3__tar.gz → 0.29.1__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 (77) hide show
  1. {httpdbg-0.28.3 → httpdbg-0.29.1}/PKG-INFO +2 -2
  2. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/__init__.py +1 -1
  3. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/env.py +1 -0
  4. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/aiohttp.py +5 -4
  5. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/external.py +3 -1
  6. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/httpx.py +10 -8
  7. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/pytest.py +29 -1
  8. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/requests.py +5 -4
  9. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/socket.py +5 -5
  10. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/urllib3.py +5 -4
  11. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/initiator.py +69 -19
  12. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/records.py +22 -24
  13. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/api.py +22 -4
  14. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/api.js +12 -2
  15. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/configuration.js +3 -3
  16. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/filter.js +14 -14
  17. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/index.htm +12 -12
  18. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/main.js +1 -1
  19. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/render.js +17 -11
  20. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/style.css +1 -1
  21. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/table.css +6 -6
  22. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg.egg-info/PKG-INFO +2 -2
  23. httpdbg-0.29.1/httpdbg.egg-info/requires.txt +3 -0
  24. {httpdbg-0.28.3 → httpdbg-0.29.1}/pyproject.toml +1 -1
  25. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_hook_aiohttp.py +3 -15
  26. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_hook_httpx.py +6 -19
  27. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_hook_requests.py +3 -6
  28. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_hook_urllib3.py +32 -30
  29. httpdbg-0.29.1/tests/test_initiator.py +167 -0
  30. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_mode_module_pytest.py +54 -5
  31. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_mode_script.py +5 -12
  32. httpdbg-0.28.3/httpdbg.egg-info/requires.txt +0 -3
  33. httpdbg-0.28.3/tests/test_initiator.py +0 -172
  34. {httpdbg-0.28.3 → httpdbg-0.29.1}/LICENSE +0 -0
  35. {httpdbg-0.28.3 → httpdbg-0.29.1}/README.md +0 -0
  36. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/__main__.py +0 -0
  37. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/args.py +0 -0
  38. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/exception.py +0 -0
  39. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/__init__.py +0 -0
  40. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/all.py +0 -0
  41. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/generic.py +0 -0
  42. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/hooks/utils.py +0 -0
  43. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/log.py +0 -0
  44. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/mode_console.py +0 -0
  45. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/mode_module.py +0 -0
  46. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/mode_script.py +0 -0
  47. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/preview.py +0 -0
  48. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/server.py +0 -0
  49. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/utils.py +0 -0
  50. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/__init__.py +0 -0
  51. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/app.py +0 -0
  52. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/column.css +0 -0
  53. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/detail.css +0 -0
  54. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/favicon.ico +0 -0
  55. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  56. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  57. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  58. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  59. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  60. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  61. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  62. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  63. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  64. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  65. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  66. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg/webapp/static/mustache.min.js +0 -0
  67. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg.egg-info/SOURCES.txt +0 -0
  68. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg.egg-info/dependency_links.txt +0 -0
  69. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg.egg-info/entry_points.txt +0 -0
  70. {httpdbg-0.28.3 → httpdbg-0.29.1}/httpdbg.egg-info/top_level.txt +0 -0
  71. {httpdbg-0.28.3 → httpdbg-0.29.1}/setup.cfg +0 -0
  72. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_api.py +0 -0
  73. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_cli.py +0 -0
  74. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_context_manager.py +0 -0
  75. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_mode_console.py +0 -0
  76. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_preview.py +0 -0
  77. {httpdbg-0.28.3 → httpdbg-0.29.1}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: httpdbg
3
- Version: 0.28.3
3
+ Version: 0.29.1
4
4
  Summary: A very simple tool to debug HTTP(S) client requests
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License: Apache-2.0
@@ -24,7 +24,7 @@ Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Provides-Extra: pytest
27
- Requires-Dist: pytest-httpdbg>=0.3.1; extra == "pytest"
27
+ Requires-Dist: pytest-httpdbg>=0.6.0; extra == "pytest"
28
28
 
29
29
  # httpdbg
30
30
 
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "0.28.3"
6
+ __version__ = "0.29.1"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -1,3 +1,4 @@
1
+ HTTPDBG_CURRENT_GROUP = "HTTPDBG_CURRENT_GROUP"
1
2
  HTTPDBG_CURRENT_INITIATOR = "HTTPDBG_CURRENT_INITIATOR"
2
3
  HTTPDBG_CURRENT_TAG = "HTTPDBG_CURRENT_TAG"
3
4
  HTTPDBG_SUBPROCESS_DIR = "HTTPDBG_SUBPROCESS_DIR"
@@ -13,20 +13,21 @@ from httpdbg.records import HTTPRecords
13
13
 
14
14
  def set_hook_for_aiohttp_async(records: HTTPRecords, method: Callable):
15
15
  async def hook(*args, **kwargs):
16
- initiator = None
16
+ initiator_and_group = None
17
17
  try:
18
18
  with httpdbg_initiator(
19
19
  records, traceback.extract_stack(), method, *args, **kwargs
20
- ) as initiator:
20
+ ) as initiator_and_group:
21
21
  ret = await method(*args, **kwargs)
22
22
  return ret
23
23
  except Exception as ex:
24
24
  callargs = getcallargs(method, *args, **kwargs)
25
25
 
26
26
  if "str_or_url" in callargs:
27
- if initiator:
27
+ if initiator_and_group:
28
+ initiator, group = initiator_and_group
28
29
  records.add_new_record_exception(
29
- initiator, str(callargs["str_or_url"]), ex
30
+ initiator, group, str(callargs["str_or_url"]), ex
30
31
  )
31
32
  raise
32
33
 
@@ -46,8 +46,10 @@ class WatcherSubprocessDirThread(threading.Thread):
46
46
  def load_dump(self):
47
47
  for dump in glob.glob(f"{os.environ[HTTPDBG_SUBPROCESS_DIR]}/*.httpdbgrecords"):
48
48
  with open(dump, "rb") as dumpfile:
49
- newrecords = pickle.load(dumpfile)
49
+ newrecords: HTTPRecords = pickle.load(dumpfile)
50
50
  self.records.requests.update(newrecords.requests)
51
+ self.records.initiators.update(newrecords.initiators)
52
+ self.records.groups.update(newrecords.groups)
51
53
 
52
54
  def run(self):
53
55
  while not self.stop:
@@ -13,20 +13,21 @@ from httpdbg.records import HTTPRecords
13
13
 
14
14
  def set_hook_for_httpx_async(records: HTTPRecords, method: Callable):
15
15
  async def hook(*args, **kwargs):
16
- initiator = None
16
+ initiator_and_group = None
17
17
  try:
18
18
  with httpdbg_initiator(
19
19
  records, traceback.extract_stack(), method, *args, **kwargs
20
- ) as initiator:
20
+ ) as initiator_and_group:
21
21
  ret = await method(*args, **kwargs)
22
22
  return ret
23
23
  except Exception as ex:
24
24
  callargs = getcallargs(method, *args, **kwargs)
25
25
 
26
26
  if "url" in callargs:
27
- if initiator:
27
+ if initiator_and_group:
28
+ initiator, group = initiator_and_group
28
29
  records.add_new_record_exception(
29
- initiator, str(callargs["url"]), ex
30
+ initiator, group, str(callargs["url"]), ex
30
31
  )
31
32
  raise
32
33
 
@@ -35,20 +36,21 @@ def set_hook_for_httpx_async(records: HTTPRecords, method: Callable):
35
36
 
36
37
  def set_hook_for_httpx(records: HTTPRecords, method: Callable):
37
38
  def hook(*args, **kwargs):
38
- initiator = None
39
+ initiator_and_group = None
39
40
  try:
40
41
  with httpdbg_initiator(
41
42
  records, traceback.extract_stack(), method, *args, **kwargs
42
- ) as initiator:
43
+ ) as initiator_and_group:
43
44
  ret = method(*args, **kwargs)
44
45
  return ret
45
46
  except Exception as ex:
46
47
  callargs = getcallargs(method, *args, **kwargs)
47
48
 
48
49
  if "url" in callargs:
49
- if initiator:
50
+ if initiator_and_group:
51
+ initiator, group = initiator_and_group
50
52
  records.add_new_record_exception(
51
- initiator, str(callargs["url"]), ex
53
+ initiator, group, str(callargs["url"]), ex
52
54
  )
53
55
  raise
54
56
 
@@ -1,16 +1,20 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import decorate
6
7
  from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import undecorate
9
+ from httpdbg.initiator import httpdbg_group
8
10
  from httpdbg.initiator import httpdbg_tag
11
+ from httpdbg.log import logger
9
12
  from httpdbg.records import HTTPRecords
10
13
 
11
14
 
12
- def set_hook_for_pytest_fixture(_, method):
15
+ def set_hook_for_pytest_fixture(_: HTTPRecords, method: Callable):
13
16
  def hook(*args, **kwargs):
17
+ logger().info("SET_HOOK_FOR_PYTEST_FIXTURE")
14
18
  callargs = getcallargs(method, *args, **kwargs)
15
19
 
16
20
  if "fixturefunc" in callargs:
@@ -22,11 +26,28 @@ def set_hook_for_pytest_fixture(_, method):
22
26
  return hook
23
27
 
24
28
 
29
+ def set_hook_for_pytest_runtest(records: HTTPRecords, method: Callable):
30
+ def hook(*args, **kwargs):
31
+ logger().info("SET_HOOK_FOR_PYTEST_RUNTEST")
32
+ callargs = getcallargs(method, *args, **kwargs)
33
+
34
+ if "item" in callargs:
35
+ full_label = getattr(callargs["item"], "nodeid", "unknown test")
36
+ label = getattr(callargs["item"], "name", "unknown test")
37
+ with httpdbg_group(records, label, full_label):
38
+ return method(*args, **kwargs)
39
+ else:
40
+ return method(*args, **kwargs)
41
+
42
+ return hook
43
+
44
+
25
45
  @contextmanager
26
46
  def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
27
47
  hooks = False
28
48
  try:
29
49
  import _pytest.fixtures
50
+ import _pytest.runner
30
51
 
31
52
  _pytest.fixtures.call_fixture_func = decorate(
32
53
  records, _pytest.fixtures.call_fixture_func, set_hook_for_pytest_fixture
@@ -36,6 +57,11 @@ def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
36
57
  _pytest.fixtures._teardown_yield_fixture,
37
58
  set_hook_for_pytest_fixture,
38
59
  )
60
+
61
+ _pytest.runner.runtestprotocol = decorate(
62
+ records, _pytest.runner.runtestprotocol, set_hook_for_pytest_runtest
63
+ )
64
+
39
65
  hooks = True
40
66
  except ImportError:
41
67
  pass
@@ -49,3 +75,5 @@ def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
49
75
  _pytest.fixtures._teardown_yield_fixture = undecorate(
50
76
  _pytest.fixtures._teardown_yield_fixture
51
77
  )
78
+
79
+ _pytest.runner.runtestprotocol = undecorate(_pytest.runner.runtestprotocol)
@@ -13,20 +13,21 @@ from httpdbg.records import HTTPRecords
13
13
 
14
14
  def set_hook_for_requests(records: HTTPRecords, method: Callable):
15
15
  def hook(*args, **kwargs):
16
- initiator = None
16
+ initiator_and_group = None
17
17
  try:
18
18
  with httpdbg_initiator(
19
19
  records, traceback.extract_stack(), method, *args, **kwargs
20
- ) as initiator:
20
+ ) as initiator_and_group:
21
21
  ret = method(*args, **kwargs)
22
22
  return ret
23
23
  except Exception as ex:
24
24
  callargs = getcallargs(method, *args, **kwargs)
25
25
 
26
26
  if "url" in callargs:
27
- if initiator:
27
+ if initiator_and_group:
28
+ initiator, group = initiator_and_group
28
29
  records.add_new_record_exception(
29
- initiator, str(callargs["url"]), ex
30
+ initiator, group, str(callargs["url"]), ex
30
31
  )
31
32
  raise
32
33
 
@@ -206,7 +206,7 @@ def set_hook_for_socket_sendall(records: HTTPRecords, method: Callable):
206
206
  if http_detected:
207
207
  logger().info("SENDALL - http detected")
208
208
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
209
- socketdata.record.initiator = records.get_initiator()
209
+ socketdata.record.initiator_id = records.get_initiator()
210
210
  socketdata.record.address = socketdata.address
211
211
  socketdata.record.ssl = socketdata.ssl
212
212
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -251,7 +251,7 @@ def set_hook_for_socket_send(records: HTTPRecords, method: Callable):
251
251
  http_detected = socketdata.http_detected()
252
252
  if http_detected:
253
253
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
254
- socketdata.record.initiator = records.get_initiator()
254
+ socketdata.record.initiator_id = records.get_initiator()
255
255
  socketdata.record.address = socketdata.address
256
256
  socketdata.record.ssl = socketdata.ssl
257
257
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -315,7 +315,7 @@ def set_hook_for_sslobject_write(records: HTTPRecords, method: Callable):
315
315
  http_detected = socketdata.http_detected()
316
316
  if http_detected:
317
317
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
318
- socketdata.record.initiator = records.get_initiator()
318
+ socketdata.record.initiator_id = records.get_initiator()
319
319
  socketdata.record.address = socketdata.address
320
320
  socketdata.record.ssl = socketdata.ssl
321
321
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -413,7 +413,7 @@ def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
413
413
  )
414
414
 
415
415
  # only for async HTTP requests (not HTTPS) on Windows
416
- if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
416
+ if platform.system().lower() == "windows":
417
417
  asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate( # type: ignore
418
418
  records,
419
419
  asyncio.proactor_events._ProactorReadPipeTransport._data_received, # type: ignore
@@ -455,7 +455,7 @@ def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
455
455
  )
456
456
 
457
457
  # only for async HTTP requests (not HTTPS) on Windows
458
- if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
458
+ if platform.system().lower() == "windows":
459
459
  asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate( # type: ignore
460
460
  asyncio.proactor_events._ProactorReadPipeTransport._data_received # type: ignore
461
461
  )
@@ -13,20 +13,21 @@ from httpdbg.records import HTTPRecords
13
13
 
14
14
  def set_hook_for_urllib3(records: HTTPRecords, method: Callable):
15
15
  def hook(*args, **kwargs):
16
- initiator = None
16
+ initiator_and_group = None
17
17
  try:
18
18
  with httpdbg_initiator(
19
19
  records, traceback.extract_stack(), method, *args, **kwargs
20
- ) as initiator:
20
+ ) as initiator_and_group:
21
21
  ret = method(*args, **kwargs)
22
22
  return ret
23
23
  except Exception as ex:
24
24
  callargs = getcallargs(method, *args, **kwargs)
25
25
 
26
26
  if "url" in callargs:
27
- if initiator:
27
+ if initiator_and_group:
28
+ initiator, group = initiator_and_group
28
29
  records.add_new_record_exception(
29
- initiator, str(callargs["url"]), ex
30
+ initiator, group, str(callargs["url"]), ex
30
31
  )
31
32
  raise
32
33
 
@@ -7,7 +7,12 @@ from typing import Generator
7
7
  from typing import List
8
8
  from typing import Tuple
9
9
  from typing import Union
10
+ from typing import TYPE_CHECKING
10
11
 
12
+ if TYPE_CHECKING:
13
+ from httpdbg.records import HTTPRecords
14
+
15
+ from httpdbg.env import HTTPDBG_CURRENT_GROUP
11
16
  from httpdbg.env import HTTPDBG_CURRENT_INITIATOR
12
17
  from httpdbg.env import HTTPDBG_CURRENT_TAG
13
18
  from httpdbg.hooks.utils import getcallargs
@@ -15,26 +20,22 @@ from httpdbg.utils import get_new_uuid
15
20
  from httpdbg.log import logger
16
21
 
17
22
 
18
- class Initiator(object):
23
+ class Initiator:
19
24
  def __init__(
20
25
  self,
21
- id: str,
22
- short_label: str,
23
- long_label: Union[str, None],
26
+ label: str,
24
27
  short_stack: str,
25
28
  stack: List[str],
26
29
  ):
27
- self.id = id
28
- self.short_label = short_label
29
- self.long_label = long_label
30
+ self.id = get_new_uuid()
31
+ self.label = label
30
32
  self.short_stack = short_stack
31
33
  self.stack = stack
32
34
 
33
35
  def __eq__(self, other) -> bool:
34
36
  if type(other) is Initiator:
35
37
  return (
36
- self.short_label == other.short_label
37
- and self.long_label == other.long_label
38
+ self.label == other.label
38
39
  and self.short_stack == other.short_stack
39
40
  and self.stack == other.stack
40
41
  )
@@ -45,21 +46,29 @@ class Initiator(object):
45
46
  if full:
46
47
  json = {
47
48
  "id": self.id,
48
- "short_label": self.short_label,
49
- "long_label": self.long_label,
49
+ "label": self.label,
50
50
  "short_stack": self.short_stack,
51
51
  "stack": "\n".join(self.stack),
52
52
  }
53
53
  else:
54
54
  json = {
55
55
  "id": self.id,
56
- "short_label": self.short_label,
57
- "long_label": self.long_label,
56
+ "label": self.label,
58
57
  "short_stack": self.short_stack,
59
58
  }
60
59
  return json
61
60
 
62
61
 
62
+ class Group:
63
+ def __init__(self, label: str, full_label: str):
64
+ self.id: str = get_new_uuid()
65
+ self.label: str = label
66
+ self.full_label: str = full_label
67
+
68
+ def to_json(self) -> dict:
69
+ return {"id": self.id, "label": self.label, "full_label": self.full_label}
70
+
71
+
63
72
  def compatible_path(path: str) -> str:
64
73
  p = path
65
74
  if platform.system().lower() == "windows":
@@ -185,7 +194,7 @@ def extract_short_stack_from_file(
185
194
  @contextmanager
186
195
  def httpdbg_initiator(
187
196
  records, extracted_stack: traceback.StackSummary, original_method, *args, **kwargs
188
- ) -> Generator[Union[Initiator, None], None, None]:
197
+ ) -> Generator[Union[Tuple[Initiator, Group], None], None, None]:
189
198
  envname = f"{HTTPDBG_CURRENT_INITIATOR}_{records.id}"
190
199
 
191
200
  if not os.environ.get(envname):
@@ -205,15 +214,16 @@ def httpdbg_initiator(
205
214
  short_stack += f" {k}={v}\n"
206
215
  short_stack += ")"
207
216
 
208
- current_initiator = Initiator(
209
- get_new_uuid(), instruction, None, short_stack, stack
210
- )
211
- records._initiators[current_initiator.id] = current_initiator
217
+ current_initiator = Initiator(instruction, short_stack, stack)
218
+ records.initiators[current_initiator.id] = current_initiator
212
219
 
213
220
  os.environ[envname] = current_initiator.id
214
221
 
215
222
  try:
216
- yield current_initiator
223
+ with httpdbg_group(
224
+ records, instruction, short_stack
225
+ ) as group: # by default we group the requests by initiator
226
+ yield current_initiator, group
217
227
  except Exception:
218
228
  del os.environ[envname]
219
229
  raise
@@ -233,6 +243,7 @@ def httpdbg_tag(tag: str) -> Generator[None, None, None]:
233
243
  os.environ[HTTPDBG_CURRENT_TAG] = tag
234
244
 
235
245
  try:
246
+ logger().info(f"httpdbg_tag {tag}")
236
247
  yield
237
248
  except Exception:
238
249
  if (not tag_already_set) and (HTTPDBG_CURRENT_TAG in os.environ):
@@ -241,3 +252,42 @@ def httpdbg_tag(tag: str) -> Generator[None, None, None]:
241
252
 
242
253
  if (not tag_already_set) and (HTTPDBG_CURRENT_TAG in os.environ):
243
254
  del os.environ[HTTPDBG_CURRENT_TAG]
255
+
256
+
257
+ @contextmanager
258
+ def httpdbg_group(
259
+ records: "HTTPRecords", label: str, full_label: str
260
+ ) -> Generator[Group, None, None]:
261
+
262
+ # A group is considered set if the environment variable exists and the group
263
+ # is recorded.If the environment variable exists but the group is not recorded,
264
+ # it means httpdbg was called reentrantly, and we need to record the group again
265
+ # using the same id.
266
+ group_already_set = (HTTPDBG_CURRENT_GROUP in os.environ) and (
267
+ os.environ[HTTPDBG_CURRENT_GROUP] in records.groups
268
+ )
269
+
270
+ if not group_already_set:
271
+ logger().info("httpdbg_group (new)")
272
+ group = Group(label, full_label)
273
+ # in case of a reentrant call to httpdbg, we force the group id to be the same
274
+ if HTTPDBG_CURRENT_GROUP in os.environ:
275
+ group.id = os.environ[HTTPDBG_CURRENT_GROUP]
276
+ logger().info("httpdbg_group (reentrant) keep id {group.id}")
277
+ records.groups[group.id] = group
278
+ os.environ[HTTPDBG_CURRENT_GROUP] = group.id
279
+ else:
280
+ group = records.groups[os.environ[HTTPDBG_CURRENT_GROUP]]
281
+
282
+ try:
283
+ logger().info(
284
+ f"httpdbg_group {records.groups} group_id={os.environ[HTTPDBG_CURRENT_GROUP]} label={label} full_label={full_label}"
285
+ )
286
+ yield group
287
+ except Exception:
288
+ if (not group_already_set) and (HTTPDBG_CURRENT_GROUP in os.environ):
289
+ del os.environ[HTTPDBG_CURRENT_GROUP]
290
+ raise
291
+
292
+ if (not group_already_set) and (HTTPDBG_CURRENT_GROUP in os.environ):
293
+ del os.environ[HTTPDBG_CURRENT_GROUP]
@@ -9,11 +9,13 @@ import traceback
9
9
  from urllib.parse import urlparse
10
10
  from typing import Dict, List, Tuple, Union
11
11
 
12
+ from httpdbg.env import HTTPDBG_CURRENT_GROUP
12
13
  from httpdbg.env import HTTPDBG_CURRENT_INITIATOR
13
14
  from httpdbg.env import HTTPDBG_CURRENT_TAG
14
15
  from httpdbg.utils import HTTPDBGCookie
15
16
  from httpdbg.utils import HTTPDBGHeader
16
17
  from httpdbg.initiator import in_lib
18
+ from httpdbg.initiator import Group
17
19
  from httpdbg.initiator import Initiator
18
20
  from httpdbg.preview import generate_preview
19
21
  from httpdbg.utils import get_new_uuid
@@ -214,13 +216,14 @@ class HTTPRecord:
214
216
  self.id = get_new_uuid()
215
217
  self.address: Tuple[str, int] = ("", 0)
216
218
  self._url: Union[str, None] = None
217
- self.initiator: Initiator = Initiator("", "", None, "", [])
219
+ self.initiator_id: Union[str, None] = None
218
220
  self.exception: Union[Exception, None] = None
219
221
  self.request: HTTPRecordRequest = HTTPRecordRequest()
220
222
  self.response: HTTPRecordResponse = HTTPRecordResponse()
221
223
  self.ssl: Union[bool, None] = None
222
224
  self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
223
- self.tag = os.environ.get(HTTPDBG_CURRENT_TAG)
225
+ self.tag: Union[str, None] = os.environ.get(HTTPDBG_CURRENT_TAG)
226
+ self.group_id: Union[str, None] = os.environ.get(HTTPDBG_CURRENT_GROUP)
224
227
  if tbegin:
225
228
  self.tbegin = tbegin
226
229
 
@@ -295,10 +298,12 @@ class HTTPRecords:
295
298
  self.reset()
296
299
 
297
300
  def reset(self) -> None:
301
+ logger().info("HTTPRecords.reset")
298
302
  self.id = get_new_uuid()
299
303
  self.requests: Dict[str, HTTPRecord] = {}
300
304
  self.requests_already_loaded = 0
301
- self._initiators: Dict[str, Initiator] = {}
305
+ self.initiators: Dict[str, Initiator] = {}
306
+ self.groups: Dict[str, Group] = {}
302
307
  self._sockets: Dict[int, SocketRawData] = {}
303
308
 
304
309
  @property
@@ -311,11 +316,11 @@ class HTTPRecords:
311
316
  def __len__(self) -> int:
312
317
  return len(self.requests)
313
318
 
314
- def get_initiator(self) -> Initiator:
319
+ def get_initiator(self) -> str:
315
320
  envname = f"{HTTPDBG_CURRENT_INITIATOR}_{self.id}"
316
321
 
317
322
  if envname in os.environ:
318
- initiator = self._initiators[os.environ[envname]]
323
+ initiator = self.initiators[os.environ[envname]]
319
324
  else:
320
325
  fullstack = traceback.format_stack()
321
326
  stack: List[str] = []
@@ -324,23 +329,13 @@ class HTTPRecords:
324
329
  break
325
330
  stack.append(line)
326
331
  long_label = stack[-1]
327
- short_label = long_label.split("\n")[1]
328
- initiator = Initiator(get_new_uuid(), short_label, None, long_label, stack)
329
-
330
- if ("PYTEST_CURRENT_TEST" in os.environ) and (
331
- "HTTPDBG_PYTEST_PLUGIN" not in os.environ
332
- ):
333
- long_label = " ".join(os.environ["PYTEST_CURRENT_TEST"].split(" ")[:-1])
334
- short_label = long_label.split("::")[-1]
335
- initiator = Initiator(
336
- f"{long_label}{self.id}",
337
- short_label,
338
- long_label,
339
- initiator.short_stack,
340
- initiator.stack,
341
- )
342
-
343
- return initiator
332
+ label = long_label.split("\n")[1]
333
+ initiator = Initiator(label, long_label, stack)
334
+
335
+ if initiator.id not in self.initiators:
336
+ self.initiators[initiator.id] = initiator
337
+
338
+ return initiator.id
344
339
 
345
340
  def get_socket_data(
346
341
  self, obj, extra_sock=None, force_new=False, request=None
@@ -415,11 +410,14 @@ class HTTPRecords:
415
410
  del self._sockets[id(obj)]
416
411
 
417
412
  def add_new_record_exception(
418
- self, initiator: Initiator, url: str, exception: Exception
413
+ self, initiator: Initiator, group: Group, url: str, exception: Exception
419
414
  ) -> HTTPRecord:
415
+ if initiator.id not in self.initiators:
416
+ self.initiators[initiator.id] = initiator
420
417
  new_record = HTTPRecord()
421
418
  new_record.url = url
422
- new_record.initiator = initiator
419
+ new_record.initiator_id = initiator.id
420
+ new_record.group_id = group.id
423
421
  new_record.exception = exception
424
422
  self.requests[new_record.id] = new_record
425
423
  return new_record
@@ -1,5 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from json import JSONEncoder
3
+ import traceback
3
4
  from typing import Any
4
5
  from typing import Dict
5
6
 
@@ -21,7 +22,8 @@ class RequestPayload(JSONEncoder):
21
22
  "response": None,
22
23
  "exception": None,
23
24
  "tag": req.tag,
24
- "initiator": req.initiator.to_json(),
25
+ "initiator_id": req.initiator_id,
26
+ "group_id": req.group_id,
25
27
  "in_progress": req.in_progress,
26
28
  }
27
29
 
@@ -52,7 +54,11 @@ class RequestPayload(JSONEncoder):
52
54
  if req.exception is not None:
53
55
  payload["exception"] = {
54
56
  "type": str(type(req.exception)),
55
- "message": str(req.exception),
57
+ "message": "".join(
58
+ traceback.format_exception(
59
+ type(req.exception), req.exception, req.exception.__traceback__
60
+ )
61
+ ),
56
62
  }
57
63
 
58
64
  return payload
@@ -64,7 +70,12 @@ class RequestListPayload(JSONEncoder):
64
70
  records, HTTPRecords
65
71
  ), "This encoder works only for HTTPRecords object."
66
72
 
67
- payload = {"id": records.id, "requests": {}}
73
+ payload = {
74
+ "id": records.id,
75
+ "requests": {},
76
+ "initiators": {},
77
+ "groups": {},
78
+ }
68
79
 
69
80
  for id, req in records.requests.items():
70
81
  payload["requests"][id] = {
@@ -76,10 +87,17 @@ class RequestListPayload(JSONEncoder):
76
87
  "reason": req.reason,
77
88
  "verb": req.method,
78
89
  "tag": req.tag,
79
- "initiator": req.initiator.to_json(full=False),
90
+ "initiator_id": req.initiator_id,
91
+ "group_id": req.group_id,
80
92
  "in_progress": req.in_progress,
81
93
  "tbegin": req.tbegin.isoformat(),
82
94
  "last_update": req.last_update.isoformat(),
83
95
  }
84
96
 
97
+ for id, initiator in records.initiators.items():
98
+ payload["initiators"][id] = initiator.to_json()
99
+
100
+ for id, group in records.groups.items():
101
+ payload["groups"][id] = group.to_json()
102
+
85
103
  return payload
@@ -3,8 +3,10 @@
3
3
  const global = {
4
4
  "k7": null,
5
5
  "requests": {},
6
+ "initiators": {},
7
+ "groups": {},
6
8
  "connected": false,
7
- "initiator_collapse": []
9
+ "group_collapse": []
8
10
  }
9
11
 
10
12
  function save_request(request_id, request) {
@@ -18,6 +20,8 @@ function save_request(request_id, request) {
18
20
  }
19
21
  global.requests[request_id] = request;
20
22
 
23
+ global.requests[request_id].initiator = global.initiators[request.initiator_id];
24
+
21
25
  if (global.requests[request_id].in_progress) {
22
26
  global.requests[request_id].status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$" alt="loading"/>';
23
27
  } else {
@@ -62,6 +66,11 @@ async function get_all_requests() {
62
66
  clean();
63
67
  };
64
68
 
69
+ // for the initiators and the groups, we can just save them without any verification
70
+ Object.assign(global.initiators, data.initiators);
71
+ Object.assign(global.groups, data.groups);
72
+
73
+ // for the requests, we may have to update them
65
74
  for (const [request_id, request] of Object.entries(data.requests)) {
66
75
  if (!(request_id in global.requests)) {
67
76
  // this is a new request
@@ -109,7 +118,8 @@ async function get_request(request_id) {
109
118
  }
110
119
 
111
120
  // the full stack is not present in request summary
112
- global.requests[request_id].initiator = data.initiator;
121
+ global.requests[request_id].initiator_id = data.initiator_id;
122
+ global.requests[request_id].exception = data.exception;
113
123
 
114
124
  global.requests[request_id].to_refresh = true;
115
125