httpdbg 2.2.1__tar.gz → 2.4.0__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 (98) hide show
  1. {httpdbg-2.2.1 → httpdbg-2.4.0}/PKG-INFO +3 -3
  2. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/__init__.py +1 -1
  3. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/__main__.py +1 -2
  4. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/export.py +2 -5
  5. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/aiohttp.py +1 -1
  6. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/all.py +2 -3
  7. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/external.py +52 -10
  8. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/fastapi.py +2 -3
  9. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/flask.py +1 -1
  10. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/generic.py +2 -3
  11. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/h2.py +2 -3
  12. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/http.py +1 -2
  13. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/httpx.py +1 -1
  14. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/pytest.py +1 -1
  15. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/record.py +66 -5
  16. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp1.py +32 -0
  17. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp2.py +40 -0
  18. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/requests.py +1 -1
  19. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/socket.py +5 -6
  20. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/starlette.py +1 -1
  21. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/template_sitecustomize.py +39 -11
  22. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/unittest.py +1 -1
  23. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/urllib3.py +1 -1
  24. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/uvicorn.py +1 -1
  25. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/initiator.py +24 -3
  26. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/log.py +1 -2
  27. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_console.py +3 -8
  28. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/preview.py +2 -5
  29. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/records.py +45 -4
  30. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/server.py +1 -1
  31. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/utils.py +4 -0
  32. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/app.py +2 -10
  33. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/PKG-INFO +3 -3
  34. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/SOURCES.txt +0 -1
  35. {httpdbg-2.2.1 → httpdbg-2.4.0}/pyproject.toml +3 -3
  36. httpdbg-2.2.1/httpdbg/webapp/api.py +0 -111
  37. {httpdbg-2.2.1 → httpdbg-2.4.0}/LICENSE +0 -0
  38. {httpdbg-2.2.1 → httpdbg-2.4.0}/README.md +0 -0
  39. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/args.py +0 -0
  40. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/env.py +0 -0
  41. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/exception.py +0 -0
  42. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/__init__.py +0 -0
  43. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/utils.py +0 -0
  44. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/http_status_code.py +0 -0
  45. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_module.py +0 -0
  46. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_script.py +0 -0
  47. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/__init__.py +0 -0
  48. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/api.js +0 -0
  49. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/column.css +0 -0
  50. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.css +0 -0
  51. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.js +0 -0
  52. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/detail.css +0 -0
  53. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/favicon.ico +0 -0
  54. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/filter.js +0 -0
  55. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  56. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  57. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  58. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  59. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  60. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  61. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  62. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  63. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  64. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  65. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  66. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  67. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  68. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  69. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/index.htm +0 -0
  70. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/main.js +0 -0
  71. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  72. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/render.js +0 -0
  73. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/style.css +0 -0
  74. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/table.css +0 -0
  75. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  76. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/entry_points.txt +0 -0
  77. {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/top_level.txt +0 -0
  78. {httpdbg-2.2.1 → httpdbg-2.4.0}/setup.cfg +0 -0
  79. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_api.py +0 -0
  80. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_cli.py +0 -0
  81. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_context_manager.py +0 -0
  82. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_aiohttp.py +0 -0
  83. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_h2.py +0 -0
  84. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_httpx.py +0 -0
  85. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_pytest.py +0 -0
  86. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_requests.py +0 -0
  87. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_unittest.py +0 -0
  88. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_urllib3.py +0 -0
  89. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_initiator.py +0 -0
  90. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_console.py +0 -0
  91. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_module_pytest.py +0 -0
  92. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_script.py +0 -0
  93. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_multiprocess.py +0 -0
  94. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_preview.py +0 -0
  95. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_pyhttpdbg_server.py +0 -0
  96. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_server.py +0 -0
  97. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_server_hook_fastapi.py +0 -0
  98. {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_server_hook_flask.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Summary: A very simple tool to debug HTTP(S) client and server requests.
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License-Expression: Apache-2.0
@@ -10,16 +10,16 @@ Classifier: Development Status :: 5 - Production/Stable
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
18
17
  Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
19
19
  Classifier: Topic :: Internet :: WWW/HTTP
20
20
  Classifier: Topic :: Software Development :: Testing
21
21
  Classifier: Topic :: Utilities
22
- Requires-Python: >=3.9
22
+ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Dynamic: license-file
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.1"
1
+ __version__ = "2.4.0"
2
2
 
3
3
  __all__ = ["export_html", "httprecord", "HTTPRecords"]
4
4
 
@@ -2,7 +2,6 @@ import argparse
2
2
  from pathlib import Path
3
3
  import sys
4
4
  import time
5
- from typing import Union
6
5
 
7
6
  from httpdbg import __version__
8
7
  from httpdbg.args import read_args
@@ -32,7 +31,7 @@ def pyhttpdbg(
32
31
 
33
32
  def run_recorder(
34
33
  records: HTTPRecords,
35
- initiators: Union[list[str], None],
34
+ initiators: list[str] | None,
36
35
  record_server: bool,
37
36
  test_mode: bool,
38
37
  ):
@@ -6,7 +6,6 @@ import xml.etree.ElementTree as ET
6
6
 
7
7
  import httpdbg
8
8
  from httpdbg import HTTPRecords
9
- from httpdbg.webapp.api import RequestListPayload, RequestPayload
10
9
 
11
10
 
12
11
  def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
@@ -67,12 +66,10 @@ def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
67
66
 
68
67
  # static export of the requests data
69
68
  if for_export:
70
- static_all_requests = json.dumps(
71
- records, cls=RequestListPayload, ensure_ascii=False
72
- )
69
+ static_all_requests = records.payload.decode("ascii")
73
70
  map_requests: dict[str, object] = dict()
74
71
  for record in records:
75
- map_requests[record.id] = json.loads(json.dumps(record, cls=RequestPayload))
72
+ map_requests[record.id] = json.loads(record.payload.decode("ascii"))
76
73
  static_requests: str = json.dumps(map_requests, ensure_ascii=False)
77
74
 
78
75
  def safe_for_script_tag(s: str) -> str:
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
4
 
5
5
  from httpdbg.hooks.utils import getcallargs
6
6
  from httpdbg.hooks.utils import decorate
@@ -1,6 +1,5 @@
1
+ from collections.abc import Generator
1
2
  from contextlib import contextmanager
2
- from typing import Generator
3
- from typing import Union
4
3
 
5
4
  from httpdbg.hooks.aiohttp import hook_aiohttp
6
5
  from httpdbg.hooks.external import watcher_external
@@ -24,7 +23,7 @@ from httpdbg.records import HTTPRecords
24
23
  @contextmanager
25
24
  def httprecord(
26
25
  records: HTTPRecords = None,
27
- initiators: Union[list[str], None] = None,
26
+ initiators: list[str] | None = None,
28
27
  client: bool = True,
29
28
  server: bool = False,
30
29
  ignore: tuple[tuple[str, int], ...] = (),
@@ -1,22 +1,35 @@
1
+ from collections.abc import Generator
1
2
  from contextlib import contextmanager
2
- from typing import Generator
3
+ import json
3
4
  import glob
4
5
  import os
5
- import pickle
6
6
  import tempfile
7
7
  import time
8
8
  import threading
9
- from typing import Union
10
9
 
11
10
  from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
11
+ from httpdbg.hooks.recordhttp1 import HTTP1Record
12
+ from httpdbg.hooks.recordhttp2 import HTTP2Record
13
+ from httpdbg.initiator import Group
14
+ from httpdbg.initiator import Initiator
12
15
  from httpdbg.log import logger
13
16
  from httpdbg.records import HTTPRecords
14
17
 
15
18
 
19
+ def _safe_delete_file(filename):
20
+ try:
21
+ os.remove(filename)
22
+ except Exception as ex:
23
+ logger().info(
24
+ f"_safe_delete_file - silently_catch_error - {str(ex)}",
25
+ exc_info=True,
26
+ )
27
+
28
+
16
29
  @contextmanager
17
30
  def watcher_external(
18
31
  records: HTTPRecords,
19
- initiators: Union[list[str], None] = None,
32
+ initiators: list[str] | None = None,
20
33
  server: bool = False,
21
34
  ) -> Generator[HTTPRecords, None, None]:
22
35
  if HTTPDBG_MULTIPROCESS_DIR not in os.environ:
@@ -78,12 +91,41 @@ class WatcherSubprocessDirThread(threading.Thread):
78
91
  threading.Thread.__init__(self)
79
92
 
80
93
  def load_dump(self):
81
- for dump in glob.glob(os.path.join(self.directory, "*.httpdbgrecords")):
82
- with open(dump, "rb") as dumpfile:
83
- newrecords: HTTPRecords = pickle.load(dumpfile)
84
- self.records.requests.update(newrecords.requests)
85
- self.records.initiators.update(newrecords.initiators)
86
- self.records.groups.update(newrecords.groups)
94
+ # initiators
95
+ for filename in glob.glob(os.path.join(self.directory, "*.httpdbg-initiator")):
96
+ with open(filename, "r") as f:
97
+ data = json.load(f)
98
+ initiator = Initiator.from_json(data)
99
+ self.records.initiators[initiator.id] = initiator
100
+ _safe_delete_file(filename)
101
+
102
+ # group
103
+ for filename in glob.glob(os.path.join(self.directory, "*.httpdbg-group")):
104
+ with open(filename, "r") as f:
105
+ data = json.load(f)
106
+ group = Group.from_json(data)
107
+ self.records.groups[group.id] = group
108
+ _safe_delete_file(filename)
109
+
110
+ # httprecord HTTP1
111
+ for filename in sorted(
112
+ glob.glob(os.path.join(self.directory, "*.httpdbg-http1record"))
113
+ ):
114
+ with open(filename, "r") as f:
115
+ data = json.load(f)
116
+ http1record = HTTP1Record.from_json(data)
117
+ self.records.requests[http1record.id] = http1record
118
+ _safe_delete_file(filename)
119
+
120
+ # httprecord HTTP2
121
+ for filename in sorted(
122
+ glob.glob(os.path.join(self.directory, "*.httpdbg-http2record"))
123
+ ):
124
+ with open(filename, "r") as f:
125
+ data = json.load(f)
126
+ http2record = HTTP2Record.from_json(data)
127
+ self.records.requests[http2record.id] = http2record
128
+ _safe_delete_file(filename)
87
129
 
88
130
  def run(self):
89
131
  while self._running:
@@ -1,10 +1,9 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
4
+ import datetime
3
5
  import functools
4
6
  from functools import wraps
5
- from typing import Generator
6
-
7
- import datetime
8
7
 
9
8
  from httpdbg.hooks.utils import decorate
10
9
  from httpdbg.hooks.utils import getcallargs
@@ -1,8 +1,8 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
4
  import functools
4
5
  from functools import wraps
5
- from typing import Generator
6
6
 
7
7
 
8
8
  from httpdbg.hooks.utils import getcallargs
@@ -1,13 +1,12 @@
1
1
  import builtins
2
2
  from collections.abc import Callable
3
+ from collections.abc import Generator
3
4
  from contextlib import contextmanager
4
5
  import functools
5
6
  import importlib
6
7
  import inspect
7
8
  from types import ModuleType
8
9
  from typing import Any
9
- from typing import Generator
10
- from typing import Union
11
10
 
12
11
  from httpdbg.hooks.utils import decorate
13
12
  from httpdbg.hooks.utils import undecorate
@@ -42,7 +41,7 @@ def set_hook_for_generic(records: HTTPRecords, method: Any):
42
41
 
43
42
  @contextmanager
44
43
  def hook_generic(
45
- records: HTTPRecords, initiators: Union[list[str], None] = None
44
+ records: HTTPRecords, initiators: list[str] | None = None
46
45
  ) -> Generator[None, None, None]:
47
46
  if initiators:
48
47
  # we add a hook for a generic initiator only if the module is imported
@@ -1,8 +1,7 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
4
  import random
4
- from typing import Generator
5
- from typing import Union
6
5
 
7
6
  from httpdbg.hooks.recordhttp2 import HTTP2Record
8
7
  from httpdbg.hooks.utils import decorate
@@ -67,7 +66,7 @@ class TracerHTTP2:
67
66
  stream_id: int,
68
67
  headers: list[tuple[bytes, bytes]],
69
68
  is_client: bool = True,
70
- ) -> Union[HTTP2Record, None]:
69
+ ) -> HTTP2Record | None:
71
70
  record = None
72
71
  if not is_client:
73
72
  if f"{socket_id}-{stream_id}" in self.sockets:
@@ -1,7 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
-
5
4
 
6
5
  from httpdbg.hooks.utils import decorate
7
6
  from httpdbg.hooks.utils import undecorate
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
4
 
5
5
  from httpdbg.hooks.utils import getcallargs
6
6
  from httpdbg.hooks.utils import decorate
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
4
 
5
5
  from httpdbg.hooks.utils import decorate
6
6
  from httpdbg.hooks.utils import getcallargs
@@ -1,6 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  import datetime
3
- from typing import Union
3
+ import json
4
+ import traceback
5
+ from typing import Any
4
6
  from urllib.parse import urlparse
5
7
 
6
8
  from httpdbg.utils import get_new_uuid
@@ -98,12 +100,12 @@ class HTTPRecord(ABC):
98
100
  ) -> None:
99
101
  self.id = get_new_uuid()
100
102
  self.address: tuple[str, int] = ("", 0)
101
- self._url: Union[str, None] = None
103
+ self._url: str | None = None
102
104
  self.initiator_id: str = initiator_id
103
- self.exception: Union[Exception, None] = None
104
- self.ssl: Union[bool, None] = None
105
+ self.exception: Exception | None = None
106
+ self.ssl: bool | None = None
105
107
  self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
106
- self.tag: Union[str, None] = tag
108
+ self.tag: str | None = tag
107
109
  self.group_id: str = group_id
108
110
  self.is_client: bool = is_client
109
111
  if tbegin:
@@ -177,3 +179,62 @@ class HTTPRecord(ABC):
177
179
  return self.request.protocol
178
180
  else:
179
181
  return f"{self.request.protocol} -> {self.response.protocol}"
182
+
183
+ @property
184
+ def payload(self) -> bytes:
185
+ payload: dict[str, Any] = {
186
+ "id": self.id,
187
+ "url": self.url,
188
+ "netloc": self.netloc,
189
+ "urlext": self.urlext,
190
+ "method": self.method,
191
+ "status_code": self.status_code,
192
+ "http100": self.http100,
193
+ "reason": self.reason,
194
+ "request": None,
195
+ "response": None,
196
+ "exception": None,
197
+ "tag": self.tag,
198
+ "initiator_id": self.initiator_id,
199
+ "group_id": self.group_id,
200
+ "in_progress": self.in_progress,
201
+ "is_server": not self.is_client,
202
+ }
203
+
204
+ payload["request"] = {
205
+ "headers": [header.to_json() for header in self.request.headers],
206
+ "cookies": [cookie.to_json() for cookie in self.request.cookies],
207
+ }
208
+
209
+ if self.request.content:
210
+ payload["request"]["body"] = {
211
+ "path": f"/request/{self.id}/up",
212
+ "filename": "upload",
213
+ }
214
+ payload["request"]["body"].update(self.request.preview)
215
+
216
+ payload["response"] = {
217
+ "headers": [header.to_json() for header in self.response.headers],
218
+ "cookies": [cookie.to_json() for cookie in self.response.cookies],
219
+ }
220
+
221
+ if self.response.content:
222
+ payload["response"]["body"] = {
223
+ "path": f"/request/{self.id}/down",
224
+ "filename": "download",
225
+ }
226
+ payload["response"]["body"].update(self.response.preview)
227
+
228
+ if self.exception is not None:
229
+ payload["exception"] = {
230
+ "type": str(type(self.exception)),
231
+ "message": "".join(
232
+ traceback.format_exception(
233
+ type(self.exception),
234
+ self.exception,
235
+ self.exception.__traceback__,
236
+ )
237
+ ),
238
+ }
239
+
240
+ return json.dumps(payload).encode("utf-8")
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import datetime
2
3
 
3
4
  from httpdbg.preview import generate_preview
@@ -195,3 +196,34 @@ class HTTP1Record(HTTPRecord):
195
196
  self.request.rawdata += data
196
197
  else:
197
198
  self.response.rawdata += data
199
+
200
+ def to_json(self) -> dict:
201
+ return {
202
+ "id": self.id,
203
+ "initiator_id": self.initiator_id,
204
+ "group_id": self.group_id,
205
+ "tag": self.tag,
206
+ "tbegin": self.tbegin.isoformat(),
207
+ "is_client": self.is_client,
208
+ "received": base64.b64encode(
209
+ self.response.rawdata if self.is_client else self.request.rawdata
210
+ ).decode("ascii"),
211
+ "sent": base64.b64encode(
212
+ self.request.rawdata if self.is_client else self.response.rawdata,
213
+ ).decode("ascii"),
214
+ "last_update": self.last_update.isoformat(),
215
+ }
216
+
217
+ @staticmethod
218
+ def from_json(data: dict) -> "HTTP1Record":
219
+ http1record = HTTP1Record(
220
+ data["initiator_id"],
221
+ data["group_id"],
222
+ data["tag"],
223
+ datetime.datetime.fromisoformat(data["tbegin"]),
224
+ data["is_client"],
225
+ )
226
+ http1record.id = data["id"]
227
+ http1record.receive_data(base64.b64decode(data["received"]))
228
+ http1record.send_data(base64.b64decode(data["sent"]))
229
+ return http1record
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import datetime
2
3
 
3
4
  from httpdbg.preview import generate_preview
@@ -111,3 +112,42 @@ class HTTP2Record(HTTPRecord):
111
112
  self.request.content += data
112
113
  else:
113
114
  self.response.content += data
115
+
116
+ def to_json(self) -> dict:
117
+ return {
118
+ "id": self.id,
119
+ "initiator_id": self.initiator_id,
120
+ "group_id": self.group_id,
121
+ "tag": self.tag,
122
+ "tbegin": self.tbegin.isoformat(),
123
+ "is_client": self.is_client,
124
+ "request": {
125
+ "headers": [header.to_json() for header in self.request.headers],
126
+ "content": base64.b64encode(self.request.content).decode("ascii"),
127
+ },
128
+ "response": {
129
+ "headers": [header.to_json() for header in self.response.headers],
130
+ "content": base64.b64encode(self.response.content).decode("ascii"),
131
+ },
132
+ "last_update": self.last_update.isoformat(),
133
+ }
134
+
135
+ @staticmethod
136
+ def from_json(data: dict) -> "HTTP2Record":
137
+ http2record = HTTP2Record(
138
+ data["initiator_id"],
139
+ data["group_id"],
140
+ data["tag"],
141
+ datetime.datetime.fromisoformat(data["tbegin"]),
142
+ data["is_client"],
143
+ )
144
+ http2record.id = data["id"]
145
+ http2record.request._headers = [
146
+ HTTPDBGHeader.from_json(header) for header in data["request"]["headers"]
147
+ ]
148
+ http2record.request.content = base64.b64decode(data["request"]["content"])
149
+ http2record.response._headers = [
150
+ HTTPDBGHeader.from_json(header) for header in data["response"]["headers"]
151
+ ]
152
+ http2record.response.content = base64.b64decode(data["response"]["content"])
153
+ return http2record
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
4
 
5
5
  from httpdbg.hooks.utils import getcallargs
6
6
  from httpdbg.hooks.utils import decorate
@@ -1,14 +1,13 @@
1
1
  import asyncio
2
2
  import asyncio.proactor_events
3
3
  from collections.abc import Callable
4
+ from collections.abc import Generator
4
5
  from contextlib import contextmanager
5
6
  import datetime
6
7
  import platform
7
8
  import socket
8
9
  import ssl
9
10
  import sys
10
- from typing import Generator
11
- from typing import Union
12
11
 
13
12
  from httpdbg.initiator import httpdbg_initiator
14
13
  from httpdbg.log import logger
@@ -28,7 +27,7 @@ class SocketRawData(object):
28
27
  self.address: tuple[str, int] = address
29
28
  self.ssl: bool = ssl
30
29
  self._rawdata: bytes = bytes()
31
- self.record: Union[HTTP1Record, None] = None
30
+ self.record: HTTP1Record | None = None
32
31
  self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
33
32
 
34
33
  @property
@@ -42,7 +41,7 @@ class SocketRawData(object):
42
41
  )
43
42
  self._rawdata = value
44
43
 
45
- def http_detected(self) -> Union[bool, None]:
44
+ def http_detected(self) -> bool | None:
46
45
  end_of_first_line = self.rawdata[:2048].find(b"\r\n")
47
46
  if end_of_first_line == -1:
48
47
  if len(self.rawdata) > 2048:
@@ -66,14 +65,14 @@ class TracerHTTP1:
66
65
  self,
67
66
  ignore: tuple[tuple[str, int], ...] = (),
68
67
  ):
69
- self.sockets: dict[int, Union[SocketRawData, None]] = (
68
+ self.sockets: dict[int, SocketRawData | None] = (
70
69
  {}
71
70
  ) # if None, this is not a HTTP/1 request
72
71
  self.ignore: tuple[tuple[str, int], ...] = ignore
73
72
 
74
73
  def get_socket_data(
75
74
  self, obj, extra_sock=None, force_new=False, request=None, is_uvicorn=False
76
- ) -> Union[SocketRawData, None]:
75
+ ) -> SocketRawData | None:
77
76
  """Record a new SocketRawData (or get an existing one) and return it."""
78
77
  socketdata = None
79
78
 
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
4
  from functools import wraps
4
- from typing import Generator
5
5
 
6
6
  from httpdbg.hooks.utils import decorate
7
7
  from httpdbg.hooks.utils import getcallargs
@@ -1,21 +1,30 @@
1
1
  import atexit
2
+ import json
2
3
  import os
3
- import pickle
4
4
  import sys
5
5
  import time
6
6
  import threading
7
- from typing import Union
8
7
  import uuid
9
8
 
10
9
  from httpdbg import HTTPRecords
11
10
  from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
12
11
  from httpdbg.hooks.all import httprecord
12
+ from httpdbg.hooks.record import HTTPRecord
13
+ from httpdbg.hooks.recordhttp1 import HTTP1Record
14
+ from httpdbg.hooks.recordhttp2 import HTTP2Record
13
15
  from httpdbg.log import logger
14
16
 
15
17
  HTTPDBG_INITIATORS = [] # type: ignore
16
18
  HTTPDBG_RECORD_SERVER = False
17
19
 
18
20
 
21
+ def _safe_write_json_file(filename, content):
22
+ with open(f"{filename}.tmp", "wb") as f:
23
+ f.write(json.dumps(content).encode("utf-8"))
24
+
25
+ os.replace(f"{filename}.tmp", filename)
26
+
27
+
19
28
  class HttpdbgRecorder:
20
29
  """Records the HTTP requests in subprocesses and saves them to disk."""
21
30
 
@@ -24,8 +33,7 @@ class HttpdbgRecorder:
24
33
  self.fname: str = os.path.join(
25
34
  os.environ[HTTPDBG_MULTIPROCESS_DIR], str(uuid.uuid1())
26
35
  )
27
- self.context = None
28
- self.records: Union[HTTPRecords, None] = None
36
+ self.serialized: list[str] = []
29
37
 
30
38
  def _save_to_disk_loop(self):
31
39
  while self._running:
@@ -45,19 +53,39 @@ class HttpdbgRecorder:
45
53
  time.sleep(1)
46
54
 
47
55
  def save_to_disk(self):
48
- if self.records and len(self.records.requests) > 0:
49
- with open(f"{self.fname}.httpdbgrecords.tmp", "wb") as f:
50
- pickle.dump(self.records, f)
51
- os.replace(
52
- f"{self.fname}.httpdbgrecords.tmp", f"{self.fname}.httpdbgrecords"
53
- )
56
+ # initiators
57
+ for initiator in self.records.initiators.values():
58
+ if initiator.id not in self.serialized:
59
+ self.serialized.append(initiator.id)
60
+ filename = f"{self.fname}.{initiator.id}.httpdbg-initiator"
61
+ _safe_write_json_file(filename, initiator.to_json())
62
+
63
+ # groups
64
+ for group in self.records.groups.values():
65
+ if group.id not in self.serialized:
66
+ self.serialized.append(group.id)
67
+ filename = f"{self.fname}.{group.id}.httpdbg-group"
68
+ _safe_write_json_file(filename, group.to_json())
69
+
70
+ # requests
71
+ for request in self.records.requests.values():
72
+ if isinstance(request, HTTPRecord):
73
+ marker = f"{request.id}-{request.last_update.isoformat()}"
74
+ if marker not in self.serialized:
75
+ self.serialized.append(marker)
76
+ if isinstance(request, HTTP1Record):
77
+ filename = f"{self.fname}.{marker}.httpdbg-http1record"
78
+ _safe_write_json_file(filename, request.to_json())
79
+ elif isinstance(request, HTTP2Record):
80
+ filename = f"{self.fname}.{marker}.httpdbg-http2record"
81
+ _safe_write_json_file(filename, request.to_json())
54
82
 
55
83
  def start(self):
56
84
  """Start recording."""
57
85
  self.context = httprecord(
58
86
  initiators=HTTPDBG_INITIATORS, server=HTTPDBG_RECORD_SERVER
59
87
  )
60
- self.records = self.context.__enter__()
88
+ self.records: HTTPRecords = self.context.__enter__()
61
89
  self._running = True
62
90
  self._thread = threading.Thread(target=self._save_to_disk_loop, daemon=True)
63
91
  self._thread.start()
@@ -1,9 +1,9 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
4
  import inspect
4
5
  import os
5
6
  import typing
6
- from typing import Generator
7
7
 
8
8
  from httpdbg.initiator import httpdbg_group
9
9
  from httpdbg.initiator import httpdbg_tag
@@ -1,6 +1,6 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
- from typing import Generator
4
4
 
5
5
  from httpdbg.hooks.utils import getcallargs
6
6
  from httpdbg.hooks.utils import decorate
@@ -1,8 +1,8 @@
1
1
  from collections.abc import Callable
2
+ from collections.abc import Generator
2
3
  from contextlib import contextmanager
3
4
  import datetime
4
5
  import typing
5
- from typing import Generator
6
6
 
7
7
  from httpdbg.hooks.utils import decorate
8
8
  from httpdbg.hooks.utils import undecorate