httpdbg 0.29.2__tar.gz → 0.31.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 (78) hide show
  1. {httpdbg-0.29.2 → httpdbg-0.31.0}/PKG-INFO +11 -7
  2. {httpdbg-0.29.2 → httpdbg-0.31.0}/README.md +10 -6
  3. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/__init__.py +1 -1
  4. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/all.py +4 -2
  5. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/external.py +1 -0
  6. httpdbg-0.31.0/httpdbg/hooks/unittest.py +263 -0
  7. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/initiator.py +18 -3
  8. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_script.py +11 -0
  9. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/records.py +11 -2
  10. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/api.py +8 -3
  11. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/app.py +15 -15
  12. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/api.js +27 -23
  13. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/column.css +0 -8
  14. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/configuration.js +31 -17
  15. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/index.htm +45 -63
  16. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/main.js +44 -31
  17. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/render.js +97 -28
  18. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/style.css +4 -4
  19. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/PKG-INFO +11 -7
  20. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/SOURCES.txt +3 -0
  21. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_api.py +2 -2
  22. httpdbg-0.31.0/tests/test_hook_pytest.py +38 -0
  23. httpdbg-0.31.0/tests/test_hook_unittest.py +132 -0
  24. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_initiator.py +6 -6
  25. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_module_pytest.py +0 -25
  26. {httpdbg-0.29.2 → httpdbg-0.31.0}/LICENSE +0 -0
  27. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/__main__.py +0 -0
  28. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/args.py +0 -0
  29. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/env.py +0 -0
  30. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/exception.py +0 -0
  31. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/__init__.py +0 -0
  32. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/aiohttp.py +0 -0
  33. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/generic.py +0 -0
  34. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/httpx.py +0 -0
  35. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/pytest.py +0 -0
  36. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/requests.py +0 -0
  37. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/socket.py +0 -0
  38. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/urllib3.py +0 -0
  39. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/utils.py +0 -0
  40. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/log.py +0 -0
  41. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_console.py +0 -0
  42. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_module.py +0 -0
  43. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/preview.py +0 -0
  44. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/server.py +0 -0
  45. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/utils.py +0 -0
  46. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/__init__.py +0 -0
  47. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/detail.css +0 -0
  48. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/favicon.ico +0 -0
  49. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/filter.js +0 -0
  50. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  51. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  52. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  53. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  54. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  55. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  56. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  57. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  58. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  59. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  60. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  61. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  62. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/table.css +0 -0
  63. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  64. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/entry_points.txt +0 -0
  65. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/requires.txt +0 -0
  66. {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/top_level.txt +0 -0
  67. {httpdbg-0.29.2 → httpdbg-0.31.0}/pyproject.toml +0 -0
  68. {httpdbg-0.29.2 → httpdbg-0.31.0}/setup.cfg +0 -0
  69. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_cli.py +0 -0
  70. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_context_manager.py +0 -0
  71. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_aiohttp.py +0 -0
  72. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_httpx.py +0 -0
  73. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_requests.py +0 -0
  74. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_urllib3.py +0 -0
  75. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_console.py +0 -0
  76. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_script.py +0 -0
  77. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_preview.py +0 -0
  78. {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: httpdbg
3
- Version: 0.29.2
3
+ Version: 0.31.0
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
@@ -116,6 +116,7 @@ You can use any package as an initiator, this is not limited to HTTP requests.
116
116
  | httpx | supported |
117
117
  | aiohttp | supported |
118
118
  | pytest | supported |
119
+ | unittest | supported |
119
120
  | _your_package_ | yes, with the arg _-i your_package_ |
120
121
 
121
122
  ## configuration
@@ -147,16 +148,19 @@ options:
147
148
 
148
149
  ### web interace
149
150
 
150
- Clic on the **&#9881;** button on the top right of the page.
151
+ Some options are available to customize the UI:
151
152
 
152
- Some options are available:
153
- * Hide the netloc in the url
154
- * Hide the initiator rows
155
- * Hide the tags
153
+ * Change the strategy to group the requests.
154
+ * Hide the scheme and the network location in the url.
155
+ * Hide the group rows.
156
+ * Hide the tags.
157
+ * ...
158
+
159
+ You can also pin a request or delete all unpinned requests.
156
160
 
157
161
  To keep your configuration, bookmark the page with the full search query.
158
162
 
159
- Fox example, if you want to hide the initiator rows by default, the url will be:
163
+ Fox example, if you want to hide the group rows by default, the url will be:
160
164
  ```
161
165
  http://localhost:4909/?hi=on
162
166
  ```
@@ -88,6 +88,7 @@ You can use any package as an initiator, this is not limited to HTTP requests.
88
88
  | httpx | supported |
89
89
  | aiohttp | supported |
90
90
  | pytest | supported |
91
+ | unittest | supported |
91
92
  | _your_package_ | yes, with the arg _-i your_package_ |
92
93
 
93
94
  ## configuration
@@ -119,16 +120,19 @@ options:
119
120
 
120
121
  ### web interace
121
122
 
122
- Clic on the **&#9881;** button on the top right of the page.
123
+ Some options are available to customize the UI:
123
124
 
124
- Some options are available:
125
- * Hide the netloc in the url
126
- * Hide the initiator rows
127
- * Hide the tags
125
+ * Change the strategy to group the requests.
126
+ * Hide the scheme and the network location in the url.
127
+ * Hide the group rows.
128
+ * Hide the tags.
129
+ * ...
130
+
131
+ You can also pin a request or delete all unpinned requests.
128
132
 
129
133
  To keep your configuration, bookmark the page with the full search query.
130
134
 
131
- Fox example, if you want to hide the initiator rows by default, the url will be:
135
+ Fox example, if you want to hide the group rows by default, the url will be:
132
136
  ```
133
137
  http://localhost:4909/?hi=on
134
138
  ```
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "0.29.2"
6
+ __version__ = "0.31.0"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -11,6 +11,7 @@ from httpdbg.hooks.httpx import hook_httpx
11
11
  from httpdbg.hooks.pytest import hook_pytest
12
12
  from httpdbg.hooks.requests import hook_requests
13
13
  from httpdbg.hooks.socket import hook_socket
14
+ from httpdbg.hooks.unittest import hook_unittest
14
15
  from httpdbg.hooks.urllib3 import hook_urllib3
15
16
  from httpdbg.records import HTTPRecords
16
17
 
@@ -29,5 +30,6 @@ def httprecord(
29
30
  with hook_urllib3(records):
30
31
  with hook_aiohttp(records):
31
32
  with hook_pytest(records):
32
- with hook_generic(records, initiators):
33
- yield records
33
+ with hook_unittest(records):
34
+ with hook_generic(records, initiators):
35
+ yield records
@@ -15,6 +15,7 @@ from httpdbg.records import HTTPRecords
15
15
  @contextmanager
16
16
  def watcher_external(records: HTTPRecords) -> Generator[HTTPRecords, None, None]:
17
17
  try:
18
+ watcher = None
18
19
  if HTTPDBG_SUBPROCESS_DIR not in os.environ:
19
20
  with tempfile.TemporaryDirectory(
20
21
  prefix="httpdbg"
@@ -0,0 +1,263 @@
1
+ # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
3
+ from contextlib import contextmanager
4
+ import inspect
5
+ import os
6
+ import typing
7
+ from typing import Generator
8
+
9
+ from httpdbg.initiator import httpdbg_group
10
+ from httpdbg.initiator import httpdbg_tag
11
+ from httpdbg.hooks.utils import decorate
12
+ from httpdbg.hooks.utils import getcallargs
13
+ from httpdbg.hooks.utils import undecorate
14
+ from httpdbg.records import HTTPRecords
15
+
16
+
17
+ def set_hook_for_unittest_run(records: HTTPRecords, method: Callable):
18
+
19
+ def hook(*args, **kwargs):
20
+ callargs = getcallargs(method, *args, **kwargs)
21
+
22
+ if "self" in callargs:
23
+ test_case = callargs["self"]
24
+
25
+ label = "unknown test"
26
+ full_label = "unknown test"
27
+
28
+ method_name = getattr(test_case, "_testMethodName")
29
+ if method_name:
30
+ test_class_name = test_case.__class__.__name__
31
+ label = f"{test_class_name}::{method_name}"
32
+ full_label = label
33
+ test_module = inspect.getmodule(test_case.__class__)
34
+ test_module_path = getattr(test_module, "__file__")
35
+ if test_module_path:
36
+ test_module_path = os.path.relpath(test_module_path)
37
+ full_label = f"{test_module_path}::{full_label}"
38
+
39
+ with httpdbg_group(records, label, full_label):
40
+ ret = method(*args, **kwargs)
41
+ else:
42
+ ret = method(*args, **kwargs)
43
+ return ret
44
+
45
+ return hook
46
+
47
+
48
+ def set_hook_for_unittest_fixture(_: HTTPRecords, method: Callable):
49
+
50
+ def hook(*args, **kwargs):
51
+
52
+ if getattr(method, "__name__") == "_callSetUp":
53
+ with httpdbg_tag("setUp"):
54
+ return method(*args, **kwargs)
55
+ elif getattr(method, "__name__") == "_callTearDown":
56
+ with httpdbg_tag("tearDown"):
57
+ return method(*args, **kwargs)
58
+ else:
59
+ return method(*args, **kwargs)
60
+
61
+ return hook
62
+
63
+
64
+ def set_hook_for_unittest_class_setup(records: HTTPRecords, method: Callable):
65
+
66
+ def hook(*args, **kwargs):
67
+ callargs = getcallargs(method, *args, **kwargs)
68
+
69
+ if "test" in callargs:
70
+ test_case = callargs["test"]
71
+
72
+ test_class_name = test_case.__class__.__name__
73
+ label = f"{test_class_name}::setUpClass"
74
+ full_label = label
75
+
76
+ test_module = inspect.getmodule(test_case.__class__)
77
+ test_module_path = getattr(test_module, "__file__")
78
+ if test_module_path:
79
+ test_module_path = os.path.relpath(test_module_path)
80
+ full_label = f"{test_module_path}::{full_label}"
81
+
82
+ with httpdbg_group(records, label, full_label):
83
+ return method(*args, **kwargs)
84
+ else:
85
+ return method(*args, **kwargs)
86
+
87
+ return hook
88
+
89
+
90
+ def set_hook_for_unittest_class_teardown(records: HTTPRecords, method: Callable):
91
+
92
+ def hook(*args, **kwargs):
93
+ callargs = getcallargs(method, *args, **kwargs)
94
+
95
+ test_result = None
96
+ if "result" in callargs:
97
+ result = callargs["result"]
98
+
99
+ if result.testsRun > 0:
100
+ test_result = result
101
+
102
+ if test_result:
103
+
104
+ label = "tearDownClass (unknown class)"
105
+ full_label = label
106
+
107
+ test_class = getattr(test_result, "_previousTestClass")
108
+ if test_class:
109
+ test_class_name = test_class.__name__
110
+ label = f"{test_class_name}::tearDownClass"
111
+ full_label = label
112
+
113
+ test_module = inspect.getmodule(test_class)
114
+ test_module_path = getattr(test_module, "__file__")
115
+ if test_module_path:
116
+ test_module_path = os.path.relpath(test_module_path)
117
+ full_label = f"{test_module_path}::{full_label}"
118
+
119
+ with httpdbg_group(records, label, full_label):
120
+ return method(*args, **kwargs)
121
+ else:
122
+ return method(*args, **kwargs)
123
+
124
+ return hook
125
+
126
+
127
+ def set_hook_for_unittest_module_setup(records: HTTPRecords, method: Callable):
128
+
129
+ def hook(*args, **kwargs):
130
+ callargs = getcallargs(method, *args, **kwargs)
131
+
132
+ if "test" in callargs:
133
+ test_suite = callargs["test"]
134
+
135
+ test_module = inspect.getmodule(test_suite)
136
+
137
+ label = f"setUpModule ({test_module.__name__})" # may be __main__
138
+ full_label = label
139
+
140
+ test_module_path = getattr(test_module, "__file__")
141
+ if test_module_path:
142
+ # if we execute the tests using the script mode, the module name is __main__
143
+ # this is why we retrieve the module name from the path
144
+ module_name = os.path.splitext(os.path.basename(test_module_path))[0]
145
+ label = f"setUpModule ({module_name})"
146
+
147
+ test_module_path = os.path.relpath(test_module_path)
148
+ full_label = f"{test_module_path}::setUpModule"
149
+
150
+ with httpdbg_group(records, label, full_label):
151
+ return method(*args, **kwargs)
152
+ else:
153
+ return method(*args, **kwargs)
154
+
155
+ return hook
156
+
157
+
158
+ def set_hook_for_unittest_module_teardown(records: HTTPRecords, method: Callable):
159
+
160
+ def hook(*args, **kwargs):
161
+ callargs = getcallargs(method, *args, **kwargs)
162
+
163
+ test_result = None
164
+ if "result" in callargs:
165
+ result = callargs["result"]
166
+
167
+ if result.testsRun > 0:
168
+ test_result = result
169
+
170
+ if test_result:
171
+
172
+ label = "tearDownModule (unknown module)"
173
+ full_label = label
174
+
175
+ test_class = getattr(test_result, "_previousTestClass")
176
+ if test_class:
177
+
178
+ test_module = inspect.getmodule(test_class)
179
+
180
+ label = f"tearDownModule ({test_module.__name__})" # may be __main__
181
+
182
+ test_module_path = getattr(test_module, "__file__")
183
+ if test_module_path:
184
+ # if we execute the tests using the script mode, the module name is __main__
185
+ # this is why we retrieve the module name from the path
186
+ module_name = os.path.splitext(os.path.basename(test_module_path))[
187
+ 0
188
+ ]
189
+ label = f"tearDownModule ({module_name})"
190
+
191
+ test_module_path = os.path.relpath(test_module_path)
192
+ full_label = f"{test_module_path}::tearDownModule"
193
+
194
+ with httpdbg_group(records, label, full_label):
195
+ return method(*args, **kwargs)
196
+ else:
197
+ return method(*args, **kwargs)
198
+
199
+ return hook
200
+
201
+
202
+ @typing.no_type_check
203
+ @contextmanager
204
+ def hook_unittest(records: HTTPRecords) -> Generator[None, None, None]:
205
+ hooks = False
206
+ try:
207
+ import unittest
208
+
209
+ unittest.case.TestCase.run = decorate(
210
+ records, unittest.case.TestCase.run, set_hook_for_unittest_run
211
+ )
212
+ unittest.case.TestCase._callSetUp = decorate(
213
+ records, unittest.case.TestCase._callSetUp, set_hook_for_unittest_fixture
214
+ )
215
+ unittest.case.TestCase._callTearDown = decorate(
216
+ records, unittest.case.TestCase._callTearDown, set_hook_for_unittest_fixture
217
+ )
218
+ unittest.suite.TestSuite._handleClassSetUp = decorate(
219
+ records,
220
+ unittest.suite.TestSuite._handleClassSetUp,
221
+ set_hook_for_unittest_class_setup,
222
+ )
223
+ unittest.suite.TestSuite._tearDownPreviousClass = decorate(
224
+ records,
225
+ unittest.suite.TestSuite._tearDownPreviousClass,
226
+ set_hook_for_unittest_class_teardown,
227
+ )
228
+ unittest.suite.TestSuite._handleModuleFixture = decorate(
229
+ records,
230
+ unittest.suite.TestSuite._handleModuleFixture,
231
+ set_hook_for_unittest_module_setup,
232
+ )
233
+ unittest.suite.TestSuite._handleModuleTearDown = decorate(
234
+ records,
235
+ unittest.suite.TestSuite._handleModuleTearDown,
236
+ set_hook_for_unittest_module_teardown,
237
+ )
238
+ hooks = True
239
+ except ImportError:
240
+ pass
241
+
242
+ yield
243
+
244
+ if hooks:
245
+ unittest.case.TestCase.run = undecorate(unittest.case.TestCase.run)
246
+ unittest.case.TestCase._callSetUp = undecorate(
247
+ unittest.case.TestCase._callSetUp
248
+ )
249
+ unittest.case.TestCase._callTearDown = undecorate(
250
+ unittest.case.TestCase._callTearDown
251
+ )
252
+ unittest.suite.TestSuite._handleClassSetUp = undecorate(
253
+ unittest.suite.TestSuite._handleClassSetUp
254
+ )
255
+ unittest.suite.TestSuite._tearDownPreviousClass = undecorate(
256
+ unittest.suite.TestSuite._tearDownPreviousClass
257
+ )
258
+ unittest.suite.TestSuite._handleModuleFixture = undecorate(
259
+ unittest.suite.TestSuite._handleModuleFixture
260
+ )
261
+ unittest.suite.TestSuite._handleModuleTearDown = undecorate(
262
+ unittest.suite.TestSuite._handleModuleTearDown
263
+ )
@@ -1,5 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
4
+ import datetime
3
5
  import os
4
6
  import platform
5
7
  import traceback
@@ -31,6 +33,7 @@ class Initiator:
31
33
  self.label = label
32
34
  self.short_stack = short_stack
33
35
  self.stack = stack
36
+ self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
34
37
 
35
38
  def __eq__(self, other) -> bool:
36
39
  if type(other) is Initiator:
@@ -49,12 +52,14 @@ class Initiator:
49
52
  "label": self.label,
50
53
  "short_stack": self.short_stack,
51
54
  "stack": "\n".join(self.stack),
55
+ "tbegin": self.tbegin.isoformat(),
52
56
  }
53
57
  else:
54
58
  json = {
55
59
  "id": self.id,
56
60
  "label": self.label,
57
61
  "short_stack": self.short_stack,
62
+ "tbegin": self.tbegin.isoformat(),
58
63
  }
59
64
  return json
60
65
 
@@ -64,9 +69,15 @@ class Group:
64
69
  self.id: str = get_new_uuid()
65
70
  self.label: str = label
66
71
  self.full_label: str = full_label
72
+ self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
67
73
 
68
74
  def to_json(self) -> dict:
69
- return {"id": self.id, "label": self.label, "full_label": self.full_label}
75
+ return {
76
+ "id": self.id,
77
+ "label": self.label,
78
+ "full_label": self.full_label,
79
+ "tbegin": self.tbegin.isoformat(),
80
+ }
70
81
 
71
82
 
72
83
  def compatible_path(path: str) -> str:
@@ -193,9 +204,13 @@ def extract_short_stack_from_file(
193
204
 
194
205
  @contextmanager
195
206
  def httpdbg_initiator(
196
- records, extracted_stack: traceback.StackSummary, original_method, *args, **kwargs
207
+ records: "HTTPRecords",
208
+ extracted_stack: traceback.StackSummary,
209
+ original_method: Callable,
210
+ *args,
211
+ **kwargs,
197
212
  ) -> Generator[Union[Tuple[Initiator, Group], None], None, None]:
198
- envname = f"{HTTPDBG_CURRENT_INITIATOR}_{records.id}"
213
+ envname = f"{HTTPDBG_CURRENT_INITIATOR}_{records.session.id}"
199
214
 
200
215
  if not os.environ.get(envname):
201
216
  # temporary set a fake initiator env variable to avoid a recursion error
@@ -4,6 +4,8 @@ import sys
4
4
  import traceback
5
5
  from typing import List
6
6
 
7
+ from httpdbg.log import logger
8
+
7
9
 
8
10
  def run_script(argv: List[str]) -> None:
9
11
  sys.argv = argv
@@ -15,6 +17,15 @@ def run_script(argv: List[str]) -> None:
15
17
  except AttributeError:
16
18
  exit("script mode - error - the first argument shall be a python file")
17
19
  try:
20
+ original__main___module = sys.modules["__main__"]
21
+ sys.modules["__main__"] = module # mandatory to execute a unittest test file.
18
22
  spec.loader.exec_module(module) # type: ignore
19
23
  except Exception:
20
24
  traceback.print_exc()
25
+ except SystemExit as ex:
26
+ # in some case, for example when executing unittest tests from a script,
27
+ # the SystemExit exception is raised by the script itself so we catch it.
28
+ # TODO propagate the SystemExit value when httpdbg terminates.
29
+ logger().info(f"run_script - exception SystemExit - {str(ex)}")
30
+ finally:
31
+ sys.modules["__main__"] = original__main___module
@@ -5,6 +5,7 @@ import datetime
5
5
  import os
6
6
  import socket
7
7
  import ssl
8
+ import sys
8
9
  import traceback
9
10
  from urllib.parse import urlparse
10
11
  from typing import Dict, List, Tuple, Union
@@ -293,13 +294,21 @@ class HTTPRecord:
293
294
  return max(self.request.last_update, self.response.last_update)
294
295
 
295
296
 
297
+ class HTTPRecordsSessionInfo:
298
+
299
+ def __init__(self):
300
+ self.id: str = get_new_uuid()
301
+ self.command_line: str = " ".join(sys.argv[1:]) if sys.argv[1:] else "console"
302
+ self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
303
+
304
+
296
305
  class HTTPRecords:
297
306
  def __init__(self) -> None:
298
307
  self.reset()
299
308
 
300
309
  def reset(self) -> None:
301
310
  logger().info("HTTPRecords.reset")
302
- self.id = get_new_uuid()
311
+ self.session: HTTPRecordsSessionInfo = HTTPRecordsSessionInfo()
303
312
  self.requests: Dict[str, HTTPRecord] = {}
304
313
  self.requests_already_loaded = 0
305
314
  self.initiators: Dict[str, Initiator] = {}
@@ -317,7 +326,7 @@ class HTTPRecords:
317
326
  return len(self.requests)
318
327
 
319
328
  def get_initiator(self) -> str:
320
- envname = f"{HTTPDBG_CURRENT_INITIATOR}_{self.id}"
329
+ envname = f"{HTTPDBG_CURRENT_INITIATOR}_{self.session.id}"
321
330
 
322
331
  if envname in os.environ:
323
332
  initiator = self.initiators[os.environ[envname]]
@@ -3,6 +3,7 @@ from json import JSONEncoder
3
3
  import traceback
4
4
  from typing import Any
5
5
  from typing import Dict
6
+ from typing import Union
6
7
 
7
8
  from httpdbg.records import HTTPRecords
8
9
  from httpdbg.records import HTTPRecord
@@ -65,13 +66,17 @@ class RequestPayload(JSONEncoder):
65
66
 
66
67
 
67
68
  class RequestListPayload(JSONEncoder):
68
- def default(self, records):
69
+ def default(self, records: HTTPRecords):
69
70
  assert isinstance(
70
71
  records, HTTPRecords
71
72
  ), "This encoder works only for HTTPRecords object."
72
73
 
73
- payload = {
74
- "id": records.id,
74
+ payload: Dict[str, Dict[str, Union[str, dict]]] = {
75
+ "session": {
76
+ "id": records.session.id,
77
+ "command_line": records.session.command_line,
78
+ "tbegin": records.session.tbegin.isoformat(),
79
+ },
75
80
  "requests": {},
76
81
  "initiators": {},
77
82
  "groups": {},
@@ -11,6 +11,7 @@ from urllib.parse import urlparse, parse_qs
11
11
  from httpdbg import __version__
12
12
  from httpdbg.log import logger
13
13
  from httpdbg.webapp.api import RequestListPayload, RequestPayload
14
+ from httpdbg.records import HTTPRecords
14
15
 
15
16
 
16
17
  @contextmanager
@@ -22,7 +23,10 @@ def silently_catch_error():
22
23
 
23
24
 
24
25
  class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
25
- def __init__(self, records, *args, **kwargs):
26
+
27
+ def __init__(
28
+ self: "HttpbgHTTPRequestHandler", records: HTTPRecords, *args, **kwargs
29
+ ):
26
30
  self.records = records
27
31
  super().__init__(*args, **kwargs)
28
32
 
@@ -42,7 +46,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
42
46
  if serve(url):
43
47
  break
44
48
 
45
- def serve_static(self, url):
49
+ def serve_static(self: "HttpbgHTTPRequestHandler", url):
46
50
  base_path = os.path.dirname(os.path.realpath(__file__))
47
51
 
48
52
  if not (
@@ -99,13 +103,13 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
99
103
 
100
104
  return True
101
105
 
102
- def serve_requests(self, url):
106
+ def serve_requests(self: "HttpbgHTTPRequestHandler", url):
103
107
  if not (url.path.lower() == "/requests"):
104
108
  return False
105
109
 
106
110
  query = parse_qs(url.query)
107
111
 
108
- if query.get("id", [""])[0] == self.records.id:
112
+ if query.get("id", [""])[0] == self.records.session.id:
109
113
  self.records.requests_already_loaded = int(
110
114
  query.get("requests_already_loaded", [0])[0]
111
115
  )
@@ -122,7 +126,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
122
126
 
123
127
  return True
124
128
 
125
- def serve_request(self, url):
129
+ def serve_request(self: "HttpbgHTTPRequestHandler", url):
126
130
  regexp = r"/request/([\w\-]+)"
127
131
 
128
132
  if re.fullmatch(regexp, url.path) is None:
@@ -145,7 +149,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
145
149
 
146
150
  return True
147
151
 
148
- def serve_request_content_up(self, url):
152
+ def serve_request_content_up(self: "HttpbgHTTPRequestHandler", url):
149
153
  regexp = r"/request/([\w\-]+)/up"
150
154
 
151
155
  if re.fullmatch(regexp, url.path) is None:
@@ -162,15 +166,11 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
162
166
  self.send_header("Content-type", "application/octet-stream")
163
167
  self.send_header_no_cache()
164
168
  self.end_headers()
165
- self.wfile.write(
166
- req.request.content.encode("utf-8")
167
- if isinstance(req.request.content, str)
168
- else req.request.content
169
- )
169
+ self.wfile.write(req.request.content)
170
170
 
171
171
  return True
172
172
 
173
- def serve_request_content_down(self, url):
173
+ def serve_request_content_down(self: "HttpbgHTTPRequestHandler", url):
174
174
  regexp = r"/request/([\w\-]+)/down"
175
175
 
176
176
  if re.fullmatch(regexp, url.path) is None:
@@ -191,7 +191,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
191
191
 
192
192
  return True
193
193
 
194
- def serve_not_found(self, *kwargs):
194
+ def serve_not_found(self: "HttpbgHTTPRequestHandler", *kwargs):
195
195
  self.send_response(404)
196
196
  self.send_header_no_cache()
197
197
  self.end_headers()
@@ -199,11 +199,11 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
199
199
 
200
200
  return True
201
201
 
202
- def log_message(self, format, *args):
202
+ def log_message(self: "HttpbgHTTPRequestHandler", format, *args):
203
203
  pass
204
204
 
205
205
  def send_header_no_cache(self):
206
206
  self.send_header("Cache-Control", "max-age=0, no-cache, no-store, private")
207
207
 
208
- def send_header_with_cache(self, seconds):
208
+ def send_header_with_cache(self: "HttpbgHTTPRequestHandler", seconds):
209
209
  self.send_header("Cache-Control", f"max-age={seconds}")