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.
- {httpdbg-0.29.2 → httpdbg-0.31.0}/PKG-INFO +11 -7
- {httpdbg-0.29.2 → httpdbg-0.31.0}/README.md +10 -6
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/__init__.py +1 -1
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/all.py +4 -2
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/external.py +1 -0
- httpdbg-0.31.0/httpdbg/hooks/unittest.py +263 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/initiator.py +18 -3
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_script.py +11 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/records.py +11 -2
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/api.py +8 -3
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/app.py +15 -15
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/api.js +27 -23
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/column.css +0 -8
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/configuration.js +31 -17
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/index.htm +45 -63
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/main.js +44 -31
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/render.js +97 -28
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/style.css +4 -4
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/PKG-INFO +11 -7
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/SOURCES.txt +3 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_api.py +2 -2
- httpdbg-0.31.0/tests/test_hook_pytest.py +38 -0
- httpdbg-0.31.0/tests/test_hook_unittest.py +132 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_initiator.py +6 -6
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_module_pytest.py +0 -25
- {httpdbg-0.29.2 → httpdbg-0.31.0}/LICENSE +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/__main__.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/args.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/env.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/exception.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/__init__.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/aiohttp.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/generic.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/httpx.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/pytest.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/requests.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/socket.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/urllib3.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/hooks/utils.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/log.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_console.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/mode_module.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/preview.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/server.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/utils.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/__init__.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/detail.css +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/favicon.ico +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/filter.js +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/mustache.min.js +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg/webapp/static/table.css +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/dependency_links.txt +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/entry_points.txt +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/requires.txt +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/httpdbg.egg-info/top_level.txt +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/pyproject.toml +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/setup.cfg +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_cli.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_context_manager.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_aiohttp.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_httpx.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_requests.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_hook_urllib3.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_console.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_mode_script.py +0 -0
- {httpdbg-0.29.2 → httpdbg-0.31.0}/tests/test_preview.py +0 -0
- {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.
|
|
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
|
-
|
|
151
|
+
Some options are available to customize the UI:
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
* Hide the
|
|
154
|
-
* Hide the
|
|
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
|
|
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
|
-
|
|
123
|
+
Some options are available to customize the UI:
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
* Hide the
|
|
126
|
-
* Hide the
|
|
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
|
|
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
|
```
|
|
@@ -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
|
|
33
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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}")
|