httpdbg 0.10.0__tar.gz → 0.11.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 (67) hide show
  1. {httpdbg-0.10.0 → httpdbg-0.11.0}/PKG-INFO +28 -14
  2. {httpdbg-0.10.0 → httpdbg-0.11.0}/README.md +27 -13
  3. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/__init__.py +1 -1
  4. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/__main__.py +1 -1
  5. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/args.py +4 -0
  6. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/aiohttp.py +3 -1
  7. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/all.py +9 -2
  8. httpdbg-0.11.0/httpdbg/hooks/generic.py +158 -0
  9. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/httpx.py +9 -1
  10. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/requests.py +3 -1
  11. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/socket.py +7 -5
  12. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/urllib3.py +6 -1
  13. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/initiator.py +4 -3
  14. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg.egg-info/PKG-INFO +28 -14
  15. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg.egg-info/SOURCES.txt +1 -0
  16. {httpdbg-0.10.0 → httpdbg-0.11.0}/pyproject.toml +1 -0
  17. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_hook_aiohttp.py +5 -0
  18. httpdbg-0.11.0/tests/test_initiator.py +168 -0
  19. httpdbg-0.10.0/tests/test_initiator.py +0 -103
  20. {httpdbg-0.10.0 → httpdbg-0.11.0}/LICENSE +0 -0
  21. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/__init__.py +0 -0
  22. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/hooks/utils.py +0 -0
  23. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/mode_console.py +0 -0
  24. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/mode_module.py +0 -0
  25. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/mode_script.py +0 -0
  26. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/records.py +0 -0
  27. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/server.py +0 -0
  28. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/utils.py +0 -0
  29. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/__init__.py +0 -0
  30. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/api.py +0 -0
  31. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/app.py +0 -0
  32. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/preview.py +0 -0
  33. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/api.js +0 -0
  34. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/column.css +0 -0
  35. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/configuration.js +0 -0
  36. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/detail.css +0 -0
  37. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/favicon.ico +0 -0
  38. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/filter.js +0 -0
  39. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  40. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  41. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  42. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  43. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  44. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  45. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  46. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  47. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  48. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/index.htm +0 -0
  49. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/main.js +0 -0
  50. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  51. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/render.js +0 -0
  52. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/style.css +0 -0
  53. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg/webapp/static/table.css +0 -0
  54. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  55. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg.egg-info/entry_points.txt +0 -0
  56. {httpdbg-0.10.0 → httpdbg-0.11.0}/httpdbg.egg-info/top_level.txt +0 -0
  57. {httpdbg-0.10.0 → httpdbg-0.11.0}/setup.cfg +0 -0
  58. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_api.py +0 -0
  59. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_cli.py +0 -0
  60. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_context_manager.py +0 -0
  61. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_hook_httpx.py +0 -0
  62. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_hook_requests.py +0 -0
  63. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_hook_urllib3.py +0 -0
  64. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_mode_console.py +0 -0
  65. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_mode_module_pytest.py +0 -0
  66. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_mode_script.py +0 -0
  67. {httpdbg-0.10.0 → httpdbg-0.11.0}/tests/test_preview.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: httpdbg
3
- Version: 0.10.0
3
+ Version: 0.11.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
@@ -19,11 +19,13 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
 
21
21
  [![Build Status](https://github.com/cle-b/httpdbg/workflows/Build/badge.svg?branch=main)](https://github.com/cle-b/httpdbg/actions?query=workflow%3ABuild) [![PyPI version](https://badge.fury.io/py/httpdbg.svg)](https://badge.fury.io/py/httpdbg) [![Coverage Status](https://coveralls.io/repos/github/cle-b/httpdbg/badge.svg?branch=main)](https://coveralls.io/github/cle-b/httpdbg?branch=main)
22
+
22
23
  # httpdbg
23
24
 
24
25
  A very simple tool to debug HTTP(S) client requests.
25
26
 
26
- ![ui](ui.png)
27
+ ![](ui.png)
28
+
27
29
  ## installation
28
30
 
29
31
  ```
@@ -79,15 +81,28 @@ For example, you can view which HTTP requests are performed by `pip` when you in
79
81
  pyhttpdbg -m pip install httpdbg --upgrade
80
82
  ```
81
83
 
84
+ ## Initiators
85
+
86
+ An initiator is the function/method that is at the origin of the HTTP requests. By default, we already support some packages but you can add your own initiators.
87
+
88
+ To add a new package in the list of initiators, you can use the `-i` command line argument:
89
+
90
+ ```sh
91
+ pyhttpdbg -i api_client_pck --script my_script.py
92
+ ```
93
+
94
+ You can use any package as an initiator, this is not limited to HTTP requests.
82
95
 
83
- ## Supported libraries
96
+ ### Already supported packages
84
97
 
85
- | library | status |
86
- |------------|-------------|
87
- | requests | supported |
88
- | urllib3 | supported |
89
- | httpx | supported |
90
- | aiohttp | supported |
98
+ | packages | status |
99
+ |----------------|-------------------------------------|
100
+ | requests | supported |
101
+ | urllib3 | supported |
102
+ | httpx | supported |
103
+ | aiohttp | supported |
104
+ | pytest | supported |
105
+ | _your_package_ | yes, with the arg _-i your_package_ |
91
106
 
92
107
  ## configuration
93
108
 
@@ -96,20 +111,19 @@ No configuration is necessary to start but some few settings are available for p
96
111
  ### command line
97
112
 
98
113
  ```
99
- usage: pyhttpdbg [-h] [--port PORT] [--version] [--keep-up | --force-quit]
114
+ usage: pyhttpdbg [-h] [--port PORT] [--version] [--initiator INITIATOR] [--keep-up | --force-quit]
100
115
  [--console | --module MODULE | --script SCRIPT]
101
116
 
102
117
  httdbg - a very simple tool to debug HTTP(S) client requests
103
118
 
104
119
  options:
105
120
  -h, --help show this help message and exit
106
- --version, -v print the httpdbg version
107
-
108
121
  --port PORT, -p PORT the web interface port
109
-
122
+ --version, -v print the httpdbg version
123
+ --initiator INITIATOR, -i INITIATOR
124
+ add a new initiator (package)
110
125
  --keep-up, -k keep the server up even if the requests have been read
111
126
  --force-quit, -q stop the server even if the requests have not been read
112
-
113
127
  --console run a python console (default)
114
128
  --module MODULE, -m MODULE
115
129
  run library module as a script (the next args are passed to pytest as is)
@@ -1,9 +1,11 @@
1
1
  [![Build Status](https://github.com/cle-b/httpdbg/workflows/Build/badge.svg?branch=main)](https://github.com/cle-b/httpdbg/actions?query=workflow%3ABuild) [![PyPI version](https://badge.fury.io/py/httpdbg.svg)](https://badge.fury.io/py/httpdbg) [![Coverage Status](https://coveralls.io/repos/github/cle-b/httpdbg/badge.svg?branch=main)](https://coveralls.io/github/cle-b/httpdbg?branch=main)
2
+
2
3
  # httpdbg
3
4
 
4
5
  A very simple tool to debug HTTP(S) client requests.
5
6
 
6
- ![ui](ui.png)
7
+ ![](ui.png)
8
+
7
9
  ## installation
8
10
 
9
11
  ```
@@ -59,15 +61,28 @@ For example, you can view which HTTP requests are performed by `pip` when you in
59
61
  pyhttpdbg -m pip install httpdbg --upgrade
60
62
  ```
61
63
 
64
+ ## Initiators
65
+
66
+ An initiator is the function/method that is at the origin of the HTTP requests. By default, we already support some packages but you can add your own initiators.
67
+
68
+ To add a new package in the list of initiators, you can use the `-i` command line argument:
69
+
70
+ ```sh
71
+ pyhttpdbg -i api_client_pck --script my_script.py
72
+ ```
73
+
74
+ You can use any package as an initiator, this is not limited to HTTP requests.
62
75
 
63
- ## Supported libraries
76
+ ### Already supported packages
64
77
 
65
- | library | status |
66
- |------------|-------------|
67
- | requests | supported |
68
- | urllib3 | supported |
69
- | httpx | supported |
70
- | aiohttp | supported |
78
+ | packages | status |
79
+ |----------------|-------------------------------------|
80
+ | requests | supported |
81
+ | urllib3 | supported |
82
+ | httpx | supported |
83
+ | aiohttp | supported |
84
+ | pytest | supported |
85
+ | _your_package_ | yes, with the arg _-i your_package_ |
71
86
 
72
87
  ## configuration
73
88
 
@@ -76,20 +91,19 @@ No configuration is necessary to start but some few settings are available for p
76
91
  ### command line
77
92
 
78
93
  ```
79
- usage: pyhttpdbg [-h] [--port PORT] [--version] [--keep-up | --force-quit]
94
+ usage: pyhttpdbg [-h] [--port PORT] [--version] [--initiator INITIATOR] [--keep-up | --force-quit]
80
95
  [--console | --module MODULE | --script SCRIPT]
81
96
 
82
97
  httdbg - a very simple tool to debug HTTP(S) client requests
83
98
 
84
99
  options:
85
100
  -h, --help show this help message and exit
86
- --version, -v print the httpdbg version
87
-
88
101
  --port PORT, -p PORT the web interface port
89
-
102
+ --version, -v print the httpdbg version
103
+ --initiator INITIATOR, -i INITIATOR
104
+ add a new initiator (package)
90
105
  --keep-up, -k keep the server up even if the requests have been read
91
106
  --force-quit, -q stop the server even if the requests have not been read
92
-
93
107
  --console run a python console (default)
94
108
  --module MODULE, -m MODULE
95
109
  run library module as a script (the next args are passed to pytest as is)
@@ -2,6 +2,6 @@
2
2
  from httpdbg.hooks.all import httpdbg
3
3
  from httpdbg.utils import logger
4
4
 
5
- VERSION = "0.10.0"
5
+ VERSION = "0.11.0"
6
6
 
7
7
  __all__ = ["httpdbg", "logger"]
@@ -25,7 +25,7 @@ def pyhttpdbg(params, subparams, test_mode=False):
25
25
  sys.path.insert(0, "") # to mimic the default python command behavior
26
26
 
27
27
  with httpdbg_srv(params.port) as records:
28
- with httpdbg(records):
28
+ with httpdbg(records, params.initiator):
29
29
  if params.module:
30
30
  run_module(subparams)
31
31
  elif params.script:
@@ -23,6 +23,10 @@ def read_args(args: List[str]) -> Tuple[argparse.Namespace, List[str]]:
23
23
  "--version", "-v", action="store_true", help="print the httpdbg version"
24
24
  )
25
25
 
26
+ parser.add_argument(
27
+ "--initiator", "-i", action="append", help="add a new initiator (package)"
28
+ )
29
+
26
30
  server_state = parser.add_mutually_exclusive_group()
27
31
 
28
32
  server_state.add_argument(
@@ -1,12 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from contextlib import contextmanager
3
3
  import traceback
4
+ from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import getcallargs
6
7
  from httpdbg.hooks.utils import decorate
7
8
  from httpdbg.hooks.utils import undecorate
8
9
  from httpdbg.initiator import httpdbg_initiator
9
10
  from httpdbg.records import HTTPRecord
11
+ from httpdbg.records import HTTPRecords
10
12
 
11
13
 
12
14
  def set_hook_for_aiohttp_async(records, method):
@@ -36,7 +38,7 @@ def set_hook_for_aiohttp_async(records, method):
36
38
 
37
39
 
38
40
  @contextmanager
39
- def hook_aiohttp(records):
41
+ def hook_aiohttp(records: HTTPRecords) -> Generator[None, None, None]:
40
42
  hooks = False
41
43
  try:
42
44
  import aiohttp
@@ -1,7 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from contextlib import contextmanager
3
+ from typing import Generator
4
+ from typing import List
5
+ from typing import Union
3
6
 
4
7
  from httpdbg.hooks.aiohttp import hook_aiohttp
8
+ from httpdbg.hooks.generic import hook_generic
5
9
  from httpdbg.hooks.httpx import hook_httpx
6
10
  from httpdbg.hooks.requests import hook_requests
7
11
  from httpdbg.hooks.socket import hook_socket
@@ -10,7 +14,9 @@ from httpdbg.records import HTTPRecords
10
14
 
11
15
 
12
16
  @contextmanager
13
- def httpdbg(records=None):
17
+ def httpdbg(
18
+ records: HTTPRecords = None, initiators: Union[List[str], None] = None
19
+ ) -> Generator[HTTPRecords, None, None]:
14
20
  if records is None:
15
21
  records = HTTPRecords()
16
22
 
@@ -19,4 +25,5 @@ def httpdbg(records=None):
19
25
  with hook_requests(records):
20
26
  with hook_urllib3(records):
21
27
  with hook_aiohttp(records):
22
- yield records
28
+ with hook_generic(records, initiators):
29
+ yield records
@@ -0,0 +1,158 @@
1
+ # -*- coding: utf-8 -*-
2
+ from contextlib import contextmanager
3
+ import glob
4
+ import importlib
5
+ import inspect
6
+ from pathlib import Path
7
+ import traceback
8
+ from typing import Generator
9
+ from typing import List
10
+ from typing import Union
11
+
12
+ from httpdbg.hooks.utils import decorate
13
+ from httpdbg.hooks.utils import undecorate
14
+ from httpdbg.initiator import httpdbg_initiator
15
+ from httpdbg.records import HTTPRecords
16
+ from httpdbg.utils import logger
17
+
18
+
19
+ def set_hook_for_generic_async(records, method):
20
+ async def hook(*args, **kwargs):
21
+ with httpdbg_initiator(
22
+ records, traceback.extract_stack(), method, *args, **kwargs
23
+ ):
24
+ ret = await method(*args, **kwargs)
25
+
26
+ return ret
27
+
28
+ return hook
29
+
30
+
31
+ def set_hook_for_generic(records, method):
32
+ def hook(*args, **kwargs):
33
+ with httpdbg_initiator(
34
+ records, traceback.extract_stack(), method, *args, **kwargs
35
+ ):
36
+ ret = method(*args, **kwargs)
37
+
38
+ return ret
39
+
40
+ return hook
41
+
42
+
43
+ @contextmanager
44
+ def hook_generic(
45
+ records: HTTPRecords, initiators: Union[List[str], None] = None
46
+ ) -> Generator[None, None, None]:
47
+ hooks = []
48
+
49
+ if not initiators:
50
+ initiators = []
51
+
52
+ for initiator in initiators:
53
+ logger.debug(f"HOOK_GENERIC add package - {initiator}")
54
+ try:
55
+ hooks += list_callables_from_package(records, initiator)
56
+ except Exception as ex:
57
+ logger.debug(f"HOOK_GENERIC exception - {str(ex)}")
58
+
59
+ yield
60
+
61
+ if hooks:
62
+ for fnc in hooks:
63
+ fnc = undecorate(fnc)
64
+
65
+
66
+ def list_callables_from_package(records, package):
67
+ callables = []
68
+
69
+ callables += list_callables_from_module(records, package)
70
+
71
+ try:
72
+ imported_package = importlib.import_module(package)
73
+ for p in glob.glob(f"{list(imported_package.__path__)[0]}/*"):
74
+ file_or_dir = Path(p)
75
+ if file_or_dir.is_file():
76
+ if file_or_dir.name.endswith(".py"):
77
+ callables += list_callables_from_module(
78
+ records, f"{package}.{file_or_dir.name[:-3]}"
79
+ )
80
+ elif file_or_dir.is_dir():
81
+ if not file_or_dir.name.startswith("__"): # __pycache__
82
+ callables += list_callables_from_package(
83
+ records, f"{package}.{file_or_dir.name}"
84
+ )
85
+ except ImportError:
86
+ pass
87
+
88
+ return callables
89
+
90
+
91
+ def list_callables_from_module(records, module):
92
+ callables = []
93
+
94
+ imported_module = importlib.import_module(module)
95
+
96
+ for name in imported_module.__dict__.keys():
97
+ # hook must be applied only on function/method defined in the current module (not imported)
98
+ from_module = getattr(imported_module.__dict__[name], "__module__", "")
99
+ if from_module and from_module.startswith(module):
100
+ # a coroutine is a function so this condition must be checked first
101
+ if inspect.iscoroutinefunction(imported_module.__dict__[name]):
102
+ imported_module.__dict__[name] = decorate(
103
+ records,
104
+ imported_module.__dict__[name],
105
+ set_hook_for_generic_async,
106
+ )
107
+ callables.append(imported_module.__dict__[name])
108
+
109
+ elif inspect.isfunction(imported_module.__dict__[name]):
110
+ imported_module.__dict__[name] = decorate(
111
+ records,
112
+ imported_module.__dict__[name],
113
+ set_hook_for_generic,
114
+ )
115
+ callables.append(imported_module.__dict__[name])
116
+
117
+ elif inspect.isclass(imported_module.__dict__[name]):
118
+ callables += list_callables_from_class(
119
+ records, imported_module, module, name
120
+ )
121
+
122
+ return callables
123
+
124
+
125
+ def list_callables_from_class(records, imported_module, module, classname):
126
+ callables = []
127
+
128
+ for name in imported_module.__dict__[classname].__dict__.keys():
129
+ from_module = getattr(
130
+ imported_module.__dict__[classname].__dict__[name], "__module__", ""
131
+ )
132
+ if from_module and from_module.startswith(module):
133
+ if not name.startswith("__"):
134
+ if inspect.iscoroutinefunction(
135
+ imported_module.__dict__[classname].__dict__[name]
136
+ ):
137
+ hook = decorate(
138
+ records,
139
+ imported_module.__dict__[classname].__dict__[name],
140
+ set_hook_for_generic_async,
141
+ )
142
+ setattr(
143
+ imported_module.__dict__[classname], name, hook
144
+ ) # TypeError: 'mappingproxy' object does not support item assignment
145
+ callables.append(hook)
146
+
147
+ elif inspect.isfunction(
148
+ imported_module.__dict__[classname].__dict__[name]
149
+ ):
150
+ hook = decorate(
151
+ records,
152
+ imported_module.__dict__[classname].__dict__[name],
153
+ set_hook_for_generic,
154
+ )
155
+ setattr(imported_module.__dict__[classname], name, hook)
156
+ callables.append(hook)
157
+
158
+ return callables
@@ -1,12 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from contextlib import contextmanager
3
3
  import traceback
4
+ from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import getcallargs
6
7
  from httpdbg.hooks.utils import decorate
7
8
  from httpdbg.hooks.utils import undecorate
8
9
  from httpdbg.initiator import httpdbg_initiator
9
10
  from httpdbg.records import HTTPRecord
11
+ from httpdbg.records import HTTPRecords
10
12
 
11
13
 
12
14
  def set_hook_for_httpx_async(records, method):
@@ -62,7 +64,7 @@ def set_hook_for_httpx(records, method):
62
64
 
63
65
 
64
66
  @contextmanager
65
- def hook_httpx(records):
67
+ def hook_httpx(records: HTTPRecords) -> Generator[None, None, None]:
66
68
  hooks = False
67
69
  try:
68
70
  import httpx
@@ -88,6 +90,9 @@ def hook_httpx(records):
88
90
  httpx.AsyncClient.options = decorate(
89
91
  records, httpx.AsyncClient.options, set_hook_for_httpx_async
90
92
  )
93
+ httpx.AsyncClient.request = decorate(
94
+ records, httpx.AsyncClient.request, set_hook_for_httpx_async
95
+ )
91
96
 
92
97
  httpx.get = decorate(records, httpx.get, set_hook_for_httpx)
93
98
  httpx.post = decorate(records, httpx.post, set_hook_for_httpx)
@@ -96,6 +101,7 @@ def hook_httpx(records):
96
101
  httpx.delete = decorate(records, httpx.delete, set_hook_for_httpx)
97
102
  httpx.head = decorate(records, httpx.head, set_hook_for_httpx)
98
103
  httpx.options = decorate(records, httpx.options, set_hook_for_httpx)
104
+ httpx.request = decorate(records, httpx.request, set_hook_for_httpx)
99
105
 
100
106
  hooks = True
101
107
  except ImportError:
@@ -111,6 +117,7 @@ def hook_httpx(records):
111
117
  httpx.AsyncClient.delete = undecorate(httpx.AsyncClient.delete)
112
118
  httpx.AsyncClient.head = undecorate(httpx.AsyncClient.head)
113
119
  httpx.AsyncClient.options = undecorate(httpx.AsyncClient.options)
120
+ httpx.AsyncClient.request = undecorate(httpx.AsyncClient.request)
114
121
 
115
122
  httpx.get = undecorate(httpx.get)
116
123
  httpx.post = undecorate(httpx.post)
@@ -119,3 +126,4 @@ def hook_httpx(records):
119
126
  httpx.delete = undecorate(httpx.delete)
120
127
  httpx.head = undecorate(httpx.head)
121
128
  httpx.options = undecorate(httpx.options)
129
+ httpx.request = undecorate(httpx.request)
@@ -1,12 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from contextlib import contextmanager
3
3
  import traceback
4
+ from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import getcallargs
6
7
  from httpdbg.hooks.utils import decorate
7
8
  from httpdbg.hooks.utils import undecorate
8
9
  from httpdbg.initiator import httpdbg_initiator
9
10
  from httpdbg.records import HTTPRecord
11
+ from httpdbg.records import HTTPRecords
10
12
 
11
13
 
12
14
  def set_hook_for_requests(records, method):
@@ -36,7 +38,7 @@ def set_hook_for_requests(records, method):
36
38
 
37
39
 
38
40
  @contextmanager
39
- def hook_requests(records):
41
+ def hook_requests(records: HTTPRecords) -> Generator[None, None, None]:
40
42
  hooks = False
41
43
  try:
42
44
  import requests
@@ -6,11 +6,13 @@ import platform
6
6
  import socket
7
7
  import ssl
8
8
  import sys
9
+ from typing import Generator
9
10
 
10
11
  from httpdbg.hooks.utils import getcallargs
11
12
  from httpdbg.hooks.utils import decorate
12
13
  from httpdbg.hooks.utils import undecorate
13
14
  from httpdbg.records import HTTPRecord
15
+ from httpdbg.records import HTTPRecords
14
16
  from httpdbg.utils import logger
15
17
 
16
18
 
@@ -334,7 +336,7 @@ def set_hook_for_sslobject_read(records, method):
334
336
 
335
337
 
336
338
  @contextmanager
337
- def hook_socket(records):
339
+ def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
338
340
  socket.socket.__init__ = decorate(
339
341
  records, socket.socket.__init__, set_hook_for_socket_init
340
342
  )
@@ -386,9 +388,9 @@ def hook_socket(records):
386
388
 
387
389
  # only for async HTTP requests (not HTTPS) on Windows
388
390
  if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
389
- asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate(
391
+ asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate( # type: ignore
390
392
  records,
391
- asyncio.proactor_events._ProactorReadPipeTransport._data_received,
393
+ asyncio.proactor_events._ProactorReadPipeTransport._data_received, # type: ignore
392
394
  set_hook_for_socket_recv_into,
393
395
  )
394
396
  asyncio.proactor_events._ProactorBaseWritePipeTransport.write = decorate(
@@ -426,8 +428,8 @@ def hook_socket(records):
426
428
 
427
429
  # only for async HTTP requests (not HTTPS) on Windows
428
430
  if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
429
- asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate(
430
- asyncio.proactor_events._ProactorReadPipeTransport._data_received
431
+ asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate( # type: ignore
432
+ asyncio.proactor_events._ProactorReadPipeTransport._data_received # type: ignore
431
433
  )
432
434
  asyncio.proactor_events._ProactorBaseWritePipeTransport.write = undecorate(
433
435
  asyncio.proactor_events._ProactorBaseWritePipeTransport.write
@@ -1,12 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from contextlib import contextmanager
3
3
  import traceback
4
+ from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import getcallargs
6
7
  from httpdbg.hooks.utils import decorate
7
8
  from httpdbg.hooks.utils import undecorate
8
9
  from httpdbg.initiator import httpdbg_initiator
9
10
  from httpdbg.records import HTTPRecord
11
+ from httpdbg.records import HTTPRecords
10
12
 
11
13
 
12
14
  def set_hook_for_urllib3(records, method):
@@ -36,7 +38,7 @@ def set_hook_for_urllib3(records, method):
36
38
 
37
39
 
38
40
  @contextmanager
39
- def hook_urllib3(records):
41
+ def hook_urllib3(records: HTTPRecords) -> Generator[None, None, None]:
40
42
  hooks = False
41
43
  try:
42
44
  import urllib3
@@ -56,3 +58,6 @@ def hook_urllib3(records):
56
58
 
57
59
  if hooks:
58
60
  urllib3.PoolManager.request = undecorate(urllib3.PoolManager.request)
61
+ urllib3.HTTPConnectionPool.request = undecorate(
62
+ urllib3.HTTPConnectionPool.request
63
+ )
@@ -82,9 +82,10 @@ def get_current_instruction(
82
82
 
83
83
  n_stack = -1
84
84
  framesummary = extracted_stack[-2]
85
- if framesummary.name == "__aenter__":
86
- n_stack = -2
87
- framesummary = extracted_stack[-3]
85
+ if "asyncio" in framesummary.filename:
86
+ while "asyncio" in framesummary.filename:
87
+ n_stack -= 1
88
+ framesummary = extracted_stack[n_stack - 1]
88
89
 
89
90
  instruction = ""
90
91
  short_stack = f'File "{framesummary.filename}", line {framesummary.lineno}, in {framesummary.name}\n'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: httpdbg
3
- Version: 0.10.0
3
+ Version: 0.11.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
@@ -19,11 +19,13 @@ Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
 
21
21
  [![Build Status](https://github.com/cle-b/httpdbg/workflows/Build/badge.svg?branch=main)](https://github.com/cle-b/httpdbg/actions?query=workflow%3ABuild) [![PyPI version](https://badge.fury.io/py/httpdbg.svg)](https://badge.fury.io/py/httpdbg) [![Coverage Status](https://coveralls.io/repos/github/cle-b/httpdbg/badge.svg?branch=main)](https://coveralls.io/github/cle-b/httpdbg?branch=main)
22
+
22
23
  # httpdbg
23
24
 
24
25
  A very simple tool to debug HTTP(S) client requests.
25
26
 
26
- ![ui](ui.png)
27
+ ![](ui.png)
28
+
27
29
  ## installation
28
30
 
29
31
  ```
@@ -79,15 +81,28 @@ For example, you can view which HTTP requests are performed by `pip` when you in
79
81
  pyhttpdbg -m pip install httpdbg --upgrade
80
82
  ```
81
83
 
84
+ ## Initiators
85
+
86
+ An initiator is the function/method that is at the origin of the HTTP requests. By default, we already support some packages but you can add your own initiators.
87
+
88
+ To add a new package in the list of initiators, you can use the `-i` command line argument:
89
+
90
+ ```sh
91
+ pyhttpdbg -i api_client_pck --script my_script.py
92
+ ```
93
+
94
+ You can use any package as an initiator, this is not limited to HTTP requests.
82
95
 
83
- ## Supported libraries
96
+ ### Already supported packages
84
97
 
85
- | library | status |
86
- |------------|-------------|
87
- | requests | supported |
88
- | urllib3 | supported |
89
- | httpx | supported |
90
- | aiohttp | supported |
98
+ | packages | status |
99
+ |----------------|-------------------------------------|
100
+ | requests | supported |
101
+ | urllib3 | supported |
102
+ | httpx | supported |
103
+ | aiohttp | supported |
104
+ | pytest | supported |
105
+ | _your_package_ | yes, with the arg _-i your_package_ |
91
106
 
92
107
  ## configuration
93
108
 
@@ -96,20 +111,19 @@ No configuration is necessary to start but some few settings are available for p
96
111
  ### command line
97
112
 
98
113
  ```
99
- usage: pyhttpdbg [-h] [--port PORT] [--version] [--keep-up | --force-quit]
114
+ usage: pyhttpdbg [-h] [--port PORT] [--version] [--initiator INITIATOR] [--keep-up | --force-quit]
100
115
  [--console | --module MODULE | --script SCRIPT]
101
116
 
102
117
  httdbg - a very simple tool to debug HTTP(S) client requests
103
118
 
104
119
  options:
105
120
  -h, --help show this help message and exit
106
- --version, -v print the httpdbg version
107
-
108
121
  --port PORT, -p PORT the web interface port
109
-
122
+ --version, -v print the httpdbg version
123
+ --initiator INITIATOR, -i INITIATOR
124
+ add a new initiator (package)
110
125
  --keep-up, -k keep the server up even if the requests have been read
111
126
  --force-quit, -q stop the server even if the requests have not been read
112
-
113
127
  --console run a python console (default)
114
128
  --module MODULE, -m MODULE
115
129
  run library module as a script (the next args are passed to pytest as is)
@@ -20,6 +20,7 @@ httpdbg.egg-info/top_level.txt
20
20
  httpdbg/hooks/__init__.py
21
21
  httpdbg/hooks/aiohttp.py
22
22
  httpdbg/hooks/all.py
23
+ httpdbg/hooks/generic.py
23
24
  httpdbg/hooks/httpx.py
24
25
  httpdbg/hooks/requests.py
25
26
  httpdbg/hooks/socket.py
@@ -39,4 +39,5 @@ version = {attr = "httpdbg.VERSION"}
39
39
 
40
40
  [tool.mypy]
41
41
  no_implicit_optional = false
42
+ disable_error_code= ["method-assign",]
42
43
  files = ["httpdbg"]
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ import platform
2
3
  import sys
3
4
 
4
5
  import aiohttp
@@ -213,6 +214,10 @@ async def test_aiohttp_cookies_secure(httpbin_secure):
213
214
 
214
215
  @pytest.mark.aiohttp
215
216
  @pytest.mark.asyncio
217
+ @pytest.mark.xfail(
218
+ platform.system().lower() == "windows",
219
+ reason="flaky on Windows (sometimes only one request is recorded)",
220
+ )
216
221
  async def test_aiohttp_redirect(httpbin_both):
217
222
  with httpdbg() as records:
218
223
  async with aiohttp.ClientSession(
@@ -0,0 +1,168 @@
1
+ # -*- coding: utf-8 -*-
2
+ import asyncio
3
+ import platform
4
+ import sys
5
+
6
+ import pytest
7
+ import requests
8
+
9
+ from httpdbg.hooks.all import httpdbg
10
+
11
+
12
+ @pytest.mark.xfail(
13
+ platform.system().lower() == "windows",
14
+ reason="problem with stack view in windows",
15
+ )
16
+ @pytest.mark.initiator
17
+ def test_initiator_script(httpbin, monkeypatch):
18
+ with monkeypatch.context() as m:
19
+ m.delenv("PYTEST_CURRENT_TEST")
20
+ with httpdbg() as records:
21
+ requests.get(f"{httpbin.url}/get")
22
+
23
+ assert len(records) == 1
24
+ http_record = records[0]
25
+
26
+ assert http_record.initiator.short_label == 'requests.get(f"{httpbin.url}/get")'
27
+ assert http_record.initiator.long_label is None
28
+
29
+ assert (
30
+ """test_initiator.py", line 21, in test_initiator_script
31
+ 21. requests.get(f"{httpbin.url}/get")
32
+ ----------
33
+ requests.api.get(
34
+ url="""
35
+ + f"{httpbin.url}/get"
36
+ + """
37
+ )"""
38
+ in http_record.initiator.short_stack
39
+ )
40
+
41
+ assert (
42
+ """test_initiator.py", line 21,
43
+ 17. def test_initiator_script(httpbin, monkeypatch):
44
+ 18. with monkeypatch.context() as m:
45
+ 19. m.delenv("PYTEST_CURRENT_TEST")
46
+ 20. with httpdbg() as records:
47
+ 21. requests.get(f"{httpbin.url}/get") <====
48
+ 22.
49
+ 23. assert len(records) == 1
50
+ 24. http_record = records[0]
51
+ 25.
52
+ 26. assert http_record.initiator.short_label == \'requests.get(f"{httpbin.url}/get")\'
53
+ 27. assert http_record.initiator.long_label is None
54
+ 28.""" # noqa W291
55
+ in http_record.initiator.stack[0]
56
+ )
57
+
58
+
59
+ @pytest.mark.initiator
60
+ def test_initiator_same_line_different_initiators(httpbin, monkeypatch):
61
+ with monkeypatch.context() as m:
62
+ m.delenv("PYTEST_CURRENT_TEST")
63
+ with httpdbg() as records:
64
+ for i in range(2):
65
+ requests.get(f"{httpbin.url}/get")
66
+
67
+ assert len(records) == 2
68
+
69
+ assert records[0].initiator is not records[1].initiator
70
+ assert records[0].initiator.id != records[1].initiator.id
71
+ assert records[0].initiator == records[1].initiator
72
+
73
+
74
+ @pytest.mark.initiator
75
+ def test_initiator_redirection_same_initiator(httpbin, monkeypatch):
76
+ with monkeypatch.context() as m:
77
+ m.delenv("PYTEST_CURRENT_TEST")
78
+ with httpdbg() as records:
79
+ requests.get(
80
+ f"{httpbin.url}/redirect-to?url={httpbin.url}/get", verify=False
81
+ )
82
+
83
+ assert len(records) == 2
84
+
85
+ assert records[0].initiator is records[1].initiator
86
+
87
+
88
+ @pytest.mark.initiator
89
+ @pytest.mark.xfail(
90
+ platform.system().lower() == "windows",
91
+ reason="problem with stack view in windows",
92
+ )
93
+ def test_initiator_pytest(httpbin):
94
+ with httpdbg() as records:
95
+ requests.get(f"{httpbin.url}/get")
96
+
97
+ assert len(records) == 1
98
+
99
+ assert records[0].initiator.short_label == "test_initiator_pytest"
100
+ assert (
101
+ records[0].initiator.long_label
102
+ == "tests/test_initiator.py::test_initiator_pytest"
103
+ )
104
+ assert 'requests.get(f"{httpbin.url}/get")' in records[0].initiator.short_stack
105
+ assert 'requests.get(f"{httpbin.url}/get") <===' in records[0].initiator.stack[0]
106
+
107
+
108
+ @pytest.mark.initiator
109
+ def test_initiator_add_package_fnc(httpbin, monkeypatch):
110
+ with monkeypatch.context() as m:
111
+ m.delenv("PYTEST_CURRENT_TEST")
112
+
113
+ # all cases must be tested in the same test
114
+ with httpdbg(initiators=["initiator_pck"]) as records:
115
+ from initiator_pck.initiator2.mod2 import fnc_in_subpackage
116
+ from initiator_pck.mod1 import fnc_in_package
117
+ from initiator_pck import fnc_async
118
+ from initiator_pck.initiator2.initiator3 import fnc_in_init
119
+ from initiator_pck.initiator2.mod2 import FakeClient
120
+
121
+ fnc_in_package(f"{httpbin.url}/get")
122
+ fnc_in_subpackage(f"{httpbin.url}/get")
123
+ fnc_in_init(f"{httpbin.url}/get")
124
+ FakeClient().navigate(f"{httpbin.url}/get")
125
+ if sys.version_info >= (3, 7):
126
+ asyncio.run(fnc_async(f"{httpbin.url}/get"))
127
+
128
+ # function in a package
129
+ assert (
130
+ records[0].initiator.short_label == 'fnc_in_package(f"{httpbin.url}/get")'
131
+ )
132
+
133
+ # function in a sub package (no __init__)
134
+ assert (
135
+ records[1].initiator.short_label
136
+ == 'fnc_in_subpackage(f"{httpbin.url}/get")'
137
+ )
138
+
139
+ # function in a __init__.py file
140
+ assert records[2].initiator.short_label == 'fnc_in_init(f"{httpbin.url}/get")'
141
+
142
+ # method
143
+ assert (
144
+ records[3].initiator.short_label
145
+ == 'FakeClient().navigate(f"{httpbin.url}/get")'
146
+ )
147
+
148
+ # async function in a package
149
+ if sys.version_info >= (3, 7):
150
+ assert (
151
+ records[4].initiator.short_label
152
+ == 'asyncio.run(fnc_async(f"{httpbin.url}/get"))'
153
+ )
154
+
155
+ with httpdbg() as records:
156
+ fnc_in_package(f"{httpbin.url}/get")
157
+ fnc_in_subpackage(f"{httpbin.url}/get")
158
+ fnc_in_init(f"{httpbin.url}/get")
159
+ FakeClient().navigate(f"{httpbin.url}/get")
160
+ if sys.version_info >= (3, 7):
161
+ asyncio.run(fnc_async(f"{httpbin.url}/get"))
162
+
163
+ assert records[0].initiator.short_label == "requests.get(url)"
164
+ assert records[1].initiator.short_label == "requests.get(url) # subpackage"
165
+ assert records[2].initiator.short_label == "requests.get(url) # init"
166
+ assert records[3].initiator.short_label == "requests.get(url) # method"
167
+ if sys.version_info >= (3, 7):
168
+ assert records[4].initiator.short_label == "await client.get(url)"
@@ -1,103 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import platform
3
-
4
- import pytest
5
- import requests
6
-
7
- from httpdbg.hooks.all import httpdbg
8
-
9
-
10
- @pytest.mark.xfail(
11
- platform.system().lower() == "windows",
12
- reason="problem with stack view in windows",
13
- )
14
- @pytest.mark.initiator
15
- def test_initiator_script(httpbin, monkeypatch):
16
- with monkeypatch.context() as m:
17
- m.delenv("PYTEST_CURRENT_TEST")
18
- with httpdbg() as records:
19
- requests.get(f"{httpbin.url}/get")
20
-
21
- assert len(records) == 1
22
- http_record = records[0]
23
-
24
- assert http_record.initiator.short_label == 'requests.get(f"{httpbin.url}/get")'
25
- assert http_record.initiator.long_label is None
26
-
27
- assert (
28
- """test_initiator.py", line 19, in test_initiator_script
29
- 19. requests.get(f"{httpbin.url}/get")
30
- ----------
31
- requests.api.get(
32
- url="""
33
- + f"{httpbin.url}/get"
34
- + """
35
- )"""
36
- in http_record.initiator.short_stack
37
- )
38
-
39
- assert (
40
- """test_initiator.py", line 19,
41
- 15. def test_initiator_script(httpbin, monkeypatch):
42
- 16. with monkeypatch.context() as m:
43
- 17. m.delenv("PYTEST_CURRENT_TEST")
44
- 18. with httpdbg() as records:
45
- 19. requests.get(f"{httpbin.url}/get") <====
46
- 20.
47
- 21. assert len(records) == 1
48
- 22. http_record = records[0]
49
- 23.
50
- 24. assert http_record.initiator.short_label == \'requests.get(f"{httpbin.url}/get")\'
51
- 25. assert http_record.initiator.long_label is None
52
- 26.""" # noqa W291
53
- in http_record.initiator.stack[0]
54
- )
55
-
56
-
57
- @pytest.mark.initiator
58
- def test_initiator_same_line_different_initiators(httpbin, monkeypatch):
59
- with monkeypatch.context() as m:
60
- m.delenv("PYTEST_CURRENT_TEST")
61
- with httpdbg() as records:
62
- for i in range(2):
63
- requests.get(f"{httpbin.url}/get")
64
-
65
- assert len(records) == 2
66
-
67
- assert records[0].initiator is not records[1].initiator
68
- assert records[0].initiator.id != records[1].initiator.id
69
- assert records[0].initiator == records[1].initiator
70
-
71
-
72
- @pytest.mark.initiator
73
- def test_initiator_redirection_same_initiator(httpbin, monkeypatch):
74
- with monkeypatch.context() as m:
75
- m.delenv("PYTEST_CURRENT_TEST")
76
- with httpdbg() as records:
77
- requests.get(
78
- f"{httpbin.url}/redirect-to?url={httpbin.url}/get", verify=False
79
- )
80
-
81
- assert len(records) == 2
82
-
83
- assert records[0].initiator is records[1].initiator
84
-
85
-
86
- @pytest.mark.initiator
87
- @pytest.mark.xfail(
88
- platform.system().lower() == "windows",
89
- reason="problem with stack view in windows",
90
- )
91
- def test_initiator_pytest(httpbin):
92
- with httpdbg() as records:
93
- requests.get(f"{httpbin.url}/get")
94
-
95
- assert len(records) == 1
96
-
97
- assert records[0].initiator.short_label == "test_initiator_pytest"
98
- assert (
99
- records[0].initiator.long_label
100
- == "tests/test_initiator.py::test_initiator_pytest"
101
- )
102
- assert 'requests.get(f"{httpbin.url}/get")' in records[0].initiator.short_stack
103
- assert 'requests.get(f"{httpbin.url}/get") <===' in records[0].initiator.stack[0]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes