httpdbg 1.0.0__tar.gz → 1.1.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 (88) hide show
  1. {httpdbg-1.0.0 → httpdbg-1.1.0}/PKG-INFO +55 -14
  2. {httpdbg-1.0.0 → httpdbg-1.1.0}/README.md +53 -12
  3. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/__init__.py +1 -1
  4. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/all.py +13 -9
  5. httpdbg-1.1.0/httpdbg/hooks/flask.py +134 -0
  6. httpdbg-1.1.0/httpdbg/hooks/http.py +47 -0
  7. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/pytest.py +1 -1
  8. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/unittest.py +5 -5
  9. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/initiator.py +69 -20
  10. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/api.js +33 -30
  11. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/render.js +17 -15
  12. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg.egg-info/PKG-INFO +55 -14
  13. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg.egg-info/SOURCES.txt +3 -0
  14. {httpdbg-1.0.0 → httpdbg-1.1.0}/pyproject.toml +1 -1
  15. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_context_manager.py +14 -14
  16. httpdbg-1.1.0/tests/test_hook_flask.py +158 -0
  17. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_unittest.py +10 -10
  18. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_initiator.py +3 -3
  19. {httpdbg-1.0.0 → httpdbg-1.1.0}/LICENSE +0 -0
  20. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/__main__.py +0 -0
  21. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/args.py +0 -0
  22. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/env.py +0 -0
  23. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/exception.py +0 -0
  24. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/__init__.py +0 -0
  25. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/aiohttp.py +0 -0
  26. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/external.py +0 -0
  27. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/generic.py +0 -0
  28. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/httpx.py +0 -0
  29. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/requests.py +0 -0
  30. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/socket.py +0 -0
  31. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/template_sitecustomize.py +0 -0
  32. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/urllib3.py +0 -0
  33. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/hooks/utils.py +0 -0
  34. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/log.py +0 -0
  35. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/mode_console.py +0 -0
  36. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/mode_module.py +0 -0
  37. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/mode_script.py +0 -0
  38. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/preview.py +0 -0
  39. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/records.py +0 -0
  40. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/server.py +0 -0
  41. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/utils.py +0 -0
  42. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/__init__.py +0 -0
  43. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/api.py +0 -0
  44. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/app.py +0 -0
  45. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/column.css +0 -0
  46. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/configuration.css +0 -0
  47. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/configuration.js +0 -0
  48. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/detail.css +0 -0
  49. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/favicon.ico +0 -0
  50. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/filter.js +0 -0
  51. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  52. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  53. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  54. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  55. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  56. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  57. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  58. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  59. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  60. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  61. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  62. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  63. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  64. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  65. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  66. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/index.htm +0 -0
  67. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/main.js +0 -0
  68. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  69. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/style.css +0 -0
  70. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg/webapp/static/table.css +0 -0
  71. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  72. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg.egg-info/entry_points.txt +0 -0
  73. {httpdbg-1.0.0 → httpdbg-1.1.0}/httpdbg.egg-info/top_level.txt +0 -0
  74. {httpdbg-1.0.0 → httpdbg-1.1.0}/setup.cfg +0 -0
  75. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_api.py +0 -0
  76. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_cli.py +0 -0
  77. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_aiohttp.py +0 -0
  78. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_httpx.py +0 -0
  79. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_pytest.py +0 -0
  80. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_requests.py +0 -0
  81. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_hook_urllib3.py +0 -0
  82. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_mode_console.py +0 -0
  83. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_mode_module_pytest.py +0 -0
  84. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_mode_script.py +0 -0
  85. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_multiprocess.py +0 -0
  86. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_preview.py +0 -0
  87. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_pyhttpdbg_server.py +0 -0
  88. {httpdbg-1.0.0 → httpdbg-1.1.0}/tests/test_server.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: httpdbg
3
- Version: 1.0.0
4
- Summary: A very simple tool to debug HTTP(S) client requests
3
+ Version: 1.1.0
4
+ Summary: A very simple tool to debug HTTP(S) client and server requests.
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License: Apache-2.0
7
7
  Project-URL: Source, https://github.com/cle-b/httpdbg/
@@ -91,8 +91,32 @@ pyhttpdbg -m pytest [arg1 --arg2 ...]
91
91
 
92
92
  In that case, the requests will be grouped by test, and any requests made within a fixture or the setup/teardown methods will be identified by a tag.
93
93
 
94
- ## Initiators
95
94
 
95
+ ### HTTP server
96
+
97
+ You can trace all HTTP requests received by your HTTP server.
98
+
99
+ ```console
100
+ pyhttpdbg -m flask --app demoflask run
101
+ .... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --.
102
+ httpdbg - HTTP(S) requests available at http://localhost:4909/
103
+ .... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --.
104
+ * Serving Flask app 'demoflask'
105
+ * Debug mode: off
106
+ WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
107
+ * Running on http://127.0.0.1:5000
108
+ Press CTRL+C to quit
109
+ 127.0.0.1 - - [14/Mar/2025 17:47:42] "GET / HTTP/1.1" 200 -
110
+ 127.0.0.1 - - [14/Mar/2025 17:47:50] "GET /hello/Ben HTTP/1.1" 200 -
111
+ 127.0.0.1 - - [14/Mar/2025 17:47:56] "GET /oups HTTP/1.1" 404 -
112
+ ```
113
+
114
+ ![httpdbg 1.1.0 - pyhttpdbg -m flask --app demoflask run](https://github.com/cle-b/httpdbg/blob/main/ui_server.png?raw=true)
115
+
116
+ This feature is new and not fully implemented. See [limitations](#limitations) for more details.
117
+
118
+ ## initiators and groups
119
+
96
120
  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.
97
121
 
98
122
  To add a new package in the list of initiators, you can use the `-i` command line argument:
@@ -103,17 +127,34 @@ pyhttpdbg -i api_client_pck --script my_script.py
103
127
 
104
128
  You can use any package as an initiator, this is not limited to HTTP requests.
105
129
 
106
- ### Already supported packages for HTTP client
107
-
108
- | packages | status |
109
- |----------------|-------------------------------------|
110
- | requests | supported |
111
- | urllib3 | supported |
112
- | httpx | supported |
113
- | aiohttp | supported |
114
- | pytest | supported |
115
- | unittest | supported |
116
- | _your_package_ | yes, with the arg _-i your_package_ |
130
+ ### supported HTTP client
131
+
132
+ The initiator is the highest-level method used to make the request through the client. The group and the initiator are identical. If a request to a URL generates multiple requests, as in the case of redirection, all these requests will be grouped together.
133
+
134
+ | packages | status | initiator | group |
135
+ |----------------|-------------------------------------|-----------|-----------|
136
+ | requests | supported | yes | yes |
137
+ | urllib3 | supported | yes | yes |
138
+ | httpx | supported | yes | yes |
139
+ | aiohttp | supported | yes | yes |
140
+ | _your_package_ | yes, with the arg _-i your_package_ | yes | yes |
141
+
142
+ ### supported HTTP server
143
+
144
+ The initiator is the low-level socket method that receives the data. The group is the endpoint method used to handle the request. A client request made within the endpoint method will be included in this group.
145
+
146
+ | packages | status | initiator | group |
147
+ |----------------|-------------------------------------|-----------|-----------|
148
+ | flask | supported | no | yes |
149
+
150
+ ### supported test framework
151
+
152
+ The requests are grouped by tests.
153
+
154
+ | packages | status | initiator | group |
155
+ |----------------|-------------------------------------|-----------|-----------|
156
+ | pytest | supported | no | yes |
157
+ | unittest | supported | no | yes |
117
158
 
118
159
  ## configuration
119
160
 
@@ -65,8 +65,32 @@ pyhttpdbg -m pytest [arg1 --arg2 ...]
65
65
 
66
66
  In that case, the requests will be grouped by test, and any requests made within a fixture or the setup/teardown methods will be identified by a tag.
67
67
 
68
- ## Initiators
69
68
 
69
+ ### HTTP server
70
+
71
+ You can trace all HTTP requests received by your HTTP server.
72
+
73
+ ```console
74
+ pyhttpdbg -m flask --app demoflask run
75
+ .... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --.
76
+ httpdbg - HTTP(S) requests available at http://localhost:4909/
77
+ .... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --.
78
+ * Serving Flask app 'demoflask'
79
+ * Debug mode: off
80
+ WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
81
+ * Running on http://127.0.0.1:5000
82
+ Press CTRL+C to quit
83
+ 127.0.0.1 - - [14/Mar/2025 17:47:42] "GET / HTTP/1.1" 200 -
84
+ 127.0.0.1 - - [14/Mar/2025 17:47:50] "GET /hello/Ben HTTP/1.1" 200 -
85
+ 127.0.0.1 - - [14/Mar/2025 17:47:56] "GET /oups HTTP/1.1" 404 -
86
+ ```
87
+
88
+ ![httpdbg 1.1.0 - pyhttpdbg -m flask --app demoflask run](https://github.com/cle-b/httpdbg/blob/main/ui_server.png?raw=true)
89
+
90
+ This feature is new and not fully implemented. See [limitations](#limitations) for more details.
91
+
92
+ ## initiators and groups
93
+
70
94
  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.
71
95
 
72
96
  To add a new package in the list of initiators, you can use the `-i` command line argument:
@@ -77,17 +101,34 @@ pyhttpdbg -i api_client_pck --script my_script.py
77
101
 
78
102
  You can use any package as an initiator, this is not limited to HTTP requests.
79
103
 
80
- ### Already supported packages for HTTP client
81
-
82
- | packages | status |
83
- |----------------|-------------------------------------|
84
- | requests | supported |
85
- | urllib3 | supported |
86
- | httpx | supported |
87
- | aiohttp | supported |
88
- | pytest | supported |
89
- | unittest | supported |
90
- | _your_package_ | yes, with the arg _-i your_package_ |
104
+ ### supported HTTP client
105
+
106
+ The initiator is the highest-level method used to make the request through the client. The group and the initiator are identical. If a request to a URL generates multiple requests, as in the case of redirection, all these requests will be grouped together.
107
+
108
+ | packages | status | initiator | group |
109
+ |----------------|-------------------------------------|-----------|-----------|
110
+ | requests | supported | yes | yes |
111
+ | urllib3 | supported | yes | yes |
112
+ | httpx | supported | yes | yes |
113
+ | aiohttp | supported | yes | yes |
114
+ | _your_package_ | yes, with the arg _-i your_package_ | yes | yes |
115
+
116
+ ### supported HTTP server
117
+
118
+ The initiator is the low-level socket method that receives the data. The group is the endpoint method used to handle the request. A client request made within the endpoint method will be included in this group.
119
+
120
+ | packages | status | initiator | group |
121
+ |----------------|-------------------------------------|-----------|-----------|
122
+ | flask | supported | no | yes |
123
+
124
+ ### supported test framework
125
+
126
+ The requests are grouped by tests.
127
+
128
+ | packages | status | initiator | group |
129
+ |----------------|-------------------------------------|-----------|-----------|
130
+ | pytest | supported | no | yes |
131
+ | unittest | supported | no | yes |
91
132
 
92
133
  ## configuration
93
134
 
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "1.0.0"
6
+ __version__ = "1.1.0"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -8,12 +8,14 @@ from typing import Union
8
8
  from httpdbg.hooks.aiohttp import hook_aiohttp
9
9
  from httpdbg.hooks.external import watcher_external
10
10
  from httpdbg.hooks.generic import hook_generic
11
+ from httpdbg.hooks.http import hook_http
11
12
  from httpdbg.hooks.httpx import hook_httpx
12
13
  from httpdbg.hooks.pytest import hook_pytest
13
14
  from httpdbg.hooks.requests import hook_requests
14
15
  from httpdbg.hooks.socket import hook_socket
15
16
  from httpdbg.hooks.unittest import hook_unittest
16
17
  from httpdbg.hooks.urllib3 import hook_urllib3
18
+ from httpdbg.hooks.flask import hook_flask
17
19
  from httpdbg.records import HTTPRecords
18
20
 
19
21
 
@@ -29,12 +31,14 @@ def httprecord(
29
31
  records = HTTPRecords(client=client, server=server, ignore=ignore)
30
32
 
31
33
  with watcher_external(records, initiators, server):
32
- with hook_socket(records):
33
- with hook_httpx(records):
34
- with hook_requests(records):
35
- with hook_urllib3(records):
36
- with hook_aiohttp(records):
37
- with hook_pytest(records):
38
- with hook_unittest(records):
39
- with hook_generic(records, initiators):
40
- yield records
34
+ with hook_flask(records):
35
+ with hook_socket(records):
36
+ with hook_http(records):
37
+ with hook_httpx(records):
38
+ with hook_requests(records):
39
+ with hook_urllib3(records):
40
+ with hook_aiohttp(records):
41
+ with hook_pytest(records):
42
+ with hook_unittest(records):
43
+ with hook_generic(records, initiators):
44
+ yield records
@@ -0,0 +1,134 @@
1
+ # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
3
+ from contextlib import contextmanager
4
+ from functools import wraps
5
+ from typing import Generator
6
+
7
+ from httpdbg.hooks.utils import getcallargs
8
+ from httpdbg.hooks.utils import decorate
9
+ from httpdbg.hooks.utils import undecorate
10
+ from httpdbg.initiator import httpdbg_endpoint
11
+ from httpdbg.records import HTTPRecords
12
+
13
+
14
+ def set_hook_flask_endpoint(records: HTTPRecords, method: Callable):
15
+
16
+ @wraps(
17
+ method
18
+ ) # to avoid AssertionError: View function mapping is overwriting an existing endpoint function: hook
19
+ def hook(*args, **kwargs):
20
+
21
+ with httpdbg_endpoint(
22
+ records,
23
+ method,
24
+ *args,
25
+ **kwargs,
26
+ ):
27
+ ret = method(*args, **kwargs)
28
+ return ret
29
+
30
+ return hook
31
+
32
+
33
+ # we must not apply the hook more than once on a mapped endpoint function
34
+ # AssertionError: View function mapping is overwriting an existing endpoint function: xxxx
35
+ already_mapped = {}
36
+
37
+
38
+ def set_hook_flask_add_url_rule(records: HTTPRecords, method: Callable):
39
+
40
+ def hook(*args, **kwargs):
41
+
42
+ callargs = getcallargs(method, *args, **kwargs)
43
+
44
+ param = "view_func"
45
+
46
+ if param in callargs:
47
+ original_view_func = callargs[param]
48
+ if original_view_func not in already_mapped:
49
+ callargs[param] = decorate(
50
+ records, callargs[param], set_hook_flask_endpoint
51
+ )
52
+ already_mapped[original_view_func] = callargs[param]
53
+ else:
54
+ callargs[param] = already_mapped[original_view_func]
55
+
56
+ args = [callargs[param] if x == original_view_func else x for x in args]
57
+ if param in kwargs:
58
+ kwargs[param] = callargs[param]
59
+
60
+ return method(*args, **kwargs)
61
+
62
+ return hook
63
+
64
+
65
+ def set_hook_flask_register_error_handler(records: HTTPRecords, method: Callable):
66
+
67
+ def hook(*args, **kwargs):
68
+
69
+ callargs = getcallargs(method, *args, **kwargs)
70
+
71
+ param = "f"
72
+
73
+ if param in callargs:
74
+ original_view_func = callargs[param]
75
+ if original_view_func not in already_mapped:
76
+ callargs[param] = decorate(
77
+ records, callargs[param], set_hook_flask_endpoint
78
+ )
79
+ already_mapped[original_view_func] = callargs[param]
80
+ else:
81
+ callargs[param] = already_mapped[original_view_func]
82
+
83
+ args = [callargs[param] if x == original_view_func else x for x in args]
84
+ if param in kwargs:
85
+ kwargs[param] = callargs[param]
86
+
87
+ return method(*args, **kwargs)
88
+
89
+ return hook
90
+
91
+
92
+ @contextmanager
93
+ def hook_flask(records: HTTPRecords) -> Generator[None, None, None]:
94
+ hooks = False
95
+ try:
96
+ import flask
97
+
98
+ flask.Flask.add_url_rule = decorate(
99
+ records, flask.Flask.add_url_rule, set_hook_flask_add_url_rule
100
+ )
101
+
102
+ flask.Flask.register_error_handler = decorate(
103
+ records,
104
+ flask.Flask.register_error_handler,
105
+ set_hook_flask_register_error_handler,
106
+ )
107
+ flask.Flask.handle_exception = decorate(
108
+ records, flask.Flask.handle_exception, set_hook_flask_endpoint
109
+ )
110
+ flask.Flask.handle_http_exception = decorate(
111
+ records, flask.Flask.handle_http_exception, set_hook_flask_endpoint
112
+ )
113
+ flask.Flask.handle_user_exception = decorate(
114
+ records, flask.Flask.handle_user_exception, set_hook_flask_endpoint
115
+ )
116
+
117
+ hooks = True
118
+ except ImportError:
119
+ pass
120
+
121
+ yield
122
+
123
+ if hooks:
124
+ flask.Flask.add_url_rule = undecorate(flask.Flask.add_url_rule)
125
+ flask.Flask.register_error_handler = undecorate(
126
+ flask.Flask.register_error_handler
127
+ )
128
+ flask.Flask.handle_exception = undecorate(flask.Flask.handle_exception)
129
+ flask.Flask.handle_http_exception = undecorate(
130
+ flask.Flask.handle_http_exception
131
+ )
132
+ flask.Flask.handle_user_exception = undecorate(
133
+ flask.Flask.handle_user_exception
134
+ )
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
3
+ from contextlib import contextmanager
4
+ from typing import Generator
5
+
6
+
7
+ from httpdbg.hooks.utils import decorate
8
+ from httpdbg.hooks.utils import undecorate
9
+ from httpdbg.initiator import httpdbg_group
10
+ from httpdbg.records import HTTPRecords
11
+
12
+
13
+ def set_hook_for_http_server_handle_one_request(records: HTTPRecords, method: Callable):
14
+ def hook(*args, **kwargs):
15
+
16
+ # the group label/full_label will be updated when the endpoint method will be called
17
+ with httpdbg_group(records, "one request", ""):
18
+ ret = method(*args, **kwargs)
19
+
20
+ return ret
21
+
22
+ return hook
23
+
24
+
25
+ @contextmanager
26
+ def hook_http(records: HTTPRecords) -> Generator[None, None, None]:
27
+ hooks = False
28
+ try:
29
+ import http
30
+ import http.server
31
+
32
+ http.server.BaseHTTPRequestHandler.handle_one_request = decorate(
33
+ records,
34
+ http.server.BaseHTTPRequestHandler.handle_one_request,
35
+ set_hook_for_http_server_handle_one_request,
36
+ )
37
+
38
+ hooks = True
39
+ except ImportError:
40
+ pass
41
+
42
+ yield
43
+
44
+ if hooks:
45
+ http.server.BaseHTTPRequestHandler.handle_one_request = undecorate(
46
+ http.server.BaseHTTPRequestHandler.handle_one_request
47
+ )
@@ -34,7 +34,7 @@ def set_hook_for_pytest_runtest(records: HTTPRecords, method: Callable):
34
34
  if "item" in callargs:
35
35
  full_label = getattr(callargs["item"], "nodeid", "unknown test")
36
36
  label = getattr(callargs["item"], "name", "unknown test")
37
- with httpdbg_group(records, label, full_label):
37
+ with httpdbg_group(records, label, full_label, updatable=False):
38
38
  return method(*args, **kwargs)
39
39
  else:
40
40
  return method(*args, **kwargs)
@@ -36,7 +36,7 @@ def set_hook_for_unittest_run(records: HTTPRecords, method: Callable):
36
36
  test_module_path = os.path.relpath(test_module_path)
37
37
  full_label = f"{test_module_path}::{full_label}"
38
38
 
39
- with httpdbg_group(records, label, full_label):
39
+ with httpdbg_group(records, label, full_label, updatable=False):
40
40
  ret = method(*args, **kwargs)
41
41
  else:
42
42
  ret = method(*args, **kwargs)
@@ -79,7 +79,7 @@ def set_hook_for_unittest_class_setup(records: HTTPRecords, method: Callable):
79
79
  test_module_path = os.path.relpath(test_module_path)
80
80
  full_label = f"{test_module_path}::{full_label}"
81
81
 
82
- with httpdbg_group(records, label, full_label):
82
+ with httpdbg_group(records, label, full_label, updatable=False):
83
83
  return method(*args, **kwargs)
84
84
  else:
85
85
  return method(*args, **kwargs)
@@ -116,7 +116,7 @@ def set_hook_for_unittest_class_teardown(records: HTTPRecords, method: Callable)
116
116
  test_module_path = os.path.relpath(test_module_path)
117
117
  full_label = f"{test_module_path}::{full_label}"
118
118
 
119
- with httpdbg_group(records, label, full_label):
119
+ with httpdbg_group(records, label, full_label, updatable=False):
120
120
  return method(*args, **kwargs)
121
121
  else:
122
122
  return method(*args, **kwargs)
@@ -147,7 +147,7 @@ def set_hook_for_unittest_module_setup(records: HTTPRecords, method: Callable):
147
147
  test_module_path = os.path.relpath(test_module_path)
148
148
  full_label = f"{test_module_path}::setUpModule"
149
149
 
150
- with httpdbg_group(records, label, full_label):
150
+ with httpdbg_group(records, label, full_label, updatable=False):
151
151
  return method(*args, **kwargs)
152
152
  else:
153
153
  return method(*args, **kwargs)
@@ -191,7 +191,7 @@ def set_hook_for_unittest_module_teardown(records: HTTPRecords, method: Callable
191
191
  test_module_path = os.path.relpath(test_module_path)
192
192
  full_label = f"{test_module_path}::tearDownModule"
193
193
 
194
- with httpdbg_group(records, label, full_label):
194
+ with httpdbg_group(records, label, full_label, updatable=False):
195
195
  return method(*args, **kwargs)
196
196
  else:
197
197
  return method(*args, **kwargs)
@@ -2,6 +2,7 @@
2
2
  from collections.abc import Callable
3
3
  from contextlib import contextmanager
4
4
  import datetime
5
+ import inspect
5
6
  import os
6
7
  import platform
7
8
  import traceback
@@ -65,10 +66,11 @@ class Initiator:
65
66
 
66
67
 
67
68
  class Group:
68
- def __init__(self, label: str, full_label: str):
69
+ def __init__(self, label: str, full_label: str, updatable: bool):
69
70
  self.id: str = get_new_uuid()
70
71
  self.label: str = label
71
72
  self.full_label: str = full_label
73
+ self.updatable: bool = updatable
72
74
  self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
73
75
 
74
76
  def to_json(self) -> dict:
@@ -204,6 +206,36 @@ def extract_short_stack_from_file(
204
206
  return instruction, short_stack
205
207
 
206
208
 
209
+ def construct_call_str(original_method, *args, **kwargs):
210
+
211
+ def print_v(v):
212
+ if isinstance(v, str):
213
+ return f'"{v}"'
214
+ else:
215
+ return v
216
+
217
+ callargs = getcallargs(original_method, *args, **kwargs)
218
+
219
+ str_module = (
220
+ f"{original_method.__module__}."
221
+ if hasattr(original_method, "__module__")
222
+ else ""
223
+ )
224
+ str_name = (
225
+ f"{original_method.__name__}" if hasattr(original_method, "__name__") else "***"
226
+ )
227
+
228
+ if callargs:
229
+ r = f"{str_module}{str_name}(\n"
230
+ for k, v in callargs.items():
231
+ r += f" {k}={print_v(v)},\n"
232
+ r += ")"
233
+ else:
234
+ r = f"{str_module}{str_name}()"
235
+
236
+ return r
237
+
238
+
207
239
  @contextmanager
208
240
  def httpdbg_initiator(
209
241
  records: "HTTPRecords",
@@ -232,24 +264,10 @@ def httpdbg_initiator(
232
264
  original_initiator_id = os.environ[HTTPDBG_CURRENT_INITIATOR]
233
265
 
234
266
  # in any case, we create an Initiator
235
-
236
- callargs = getcallargs(original_method, *args, **kwargs)
237
267
  instruction, short_stack, stack = get_current_instruction(extracted_stack)
238
-
239
- str_module = (
240
- f"{original_method.__module__}."
241
- if hasattr(original_method, "__module__")
242
- else ""
243
- )
244
- str_name = (
245
- f"{original_method.__name__}"
246
- if hasattr(original_method, "__name__")
247
- else "***"
268
+ short_stack += "----------\n" + construct_call_str(
269
+ original_method, *args, **kwargs
248
270
  )
249
- short_stack += f"----------\n{str_module}{str_name}(\n"
250
- for k, v in callargs.items():
251
- short_stack += f" {k}={v}\n"
252
- short_stack += ")"
253
271
 
254
272
  current_initiator = Initiator(instruction, short_stack, stack)
255
273
 
@@ -302,7 +320,11 @@ def httpdbg_tag(tag: str) -> Generator[None, None, None]:
302
320
 
303
321
  @contextmanager
304
322
  def httpdbg_group(
305
- records: "HTTPRecords", label: str, full_label: str
323
+ records: "HTTPRecords",
324
+ label: str,
325
+ full_label: str,
326
+ update: bool = False,
327
+ updatable: bool = True,
306
328
  ) -> Generator[Group, None, None]:
307
329
 
308
330
  # A group is considered set if the environment variable exists and the group
@@ -315,7 +337,7 @@ def httpdbg_group(
315
337
 
316
338
  if not group_already_set:
317
339
  logger().info("httpdbg_group (new)")
318
- group = Group(label, full_label)
340
+ group = Group(label, full_label, updatable=updatable)
319
341
  # in case of a reentrant call to httpdbg, we force the group id to be the same
320
342
  if HTTPDBG_CURRENT_GROUP in os.environ:
321
343
  group.id = os.environ[HTTPDBG_CURRENT_GROUP]
@@ -325,9 +347,13 @@ def httpdbg_group(
325
347
  else:
326
348
  group = records.groups[os.environ[HTTPDBG_CURRENT_GROUP]]
327
349
 
350
+ if update and group.updatable:
351
+ # Update the label and full_label of an existing group, in case of endpoint.
352
+ group.label = label
353
+ group.full_label = full_label
328
354
  try:
329
355
  logger().info(
330
- f"httpdbg_group {records.groups} group_id={os.environ[HTTPDBG_CURRENT_GROUP]} label={label} full_label={full_label}"
356
+ f"httpdbg_group {group} group_id={os.environ[HTTPDBG_CURRENT_GROUP]} label={label} full_label={full_label}"
331
357
  )
332
358
  yield group
333
359
  except Exception:
@@ -337,3 +363,26 @@ def httpdbg_group(
337
363
 
338
364
  if (not group_already_set) and (HTTPDBG_CURRENT_GROUP in os.environ):
339
365
  del os.environ[HTTPDBG_CURRENT_GROUP]
366
+
367
+
368
+ @contextmanager
369
+ def httpdbg_endpoint(
370
+ records: "HTTPRecords", original_method: Callable, *args, **kwargs
371
+ ) -> Generator[None, None, None]:
372
+
373
+ filename = inspect.getsourcefile(original_method)
374
+ if filename:
375
+ _, lineno = inspect.getsourcelines(original_method)
376
+ instruction, s_stack = extract_short_stack_from_file(filename, lineno, 0, 8)
377
+ full_label = (
378
+ f'File "{filename}", line {lineno}, in {original_method.__name__}\n'
379
+ )
380
+ full_label += s_stack
381
+ full_label += "----------\n" + construct_call_str(
382
+ original_method, *args, **kwargs
383
+ )
384
+
385
+ with httpdbg_group(records, instruction, full_label, update=True):
386
+ yield
387
+ else:
388
+ yield
@@ -12,37 +12,40 @@ const global = {
12
12
  }
13
13
 
14
14
  function save_request(request_id, request, session_id) {
15
- request.loaded = false;
16
- request.to_refresh = true;
17
- if (request.pin == undefined) {
18
- request.pin = "";
19
- }
20
- if (request.filter == undefined) {
21
- request.filter = "---";
22
- }
23
- request.session_id = session_id;
24
- request.initiator = global.initiators[request.initiator_id];
25
-
26
- if (request.in_progress) {
27
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$" alt="loading"/>';
28
- } else {
29
- switch (request.status_code) {
30
- case 0:
31
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="loading"/>';
32
- break;
33
- case -1:
34
- request.status_code_view = '<img class="icon" src="static/icons/math-multiplication-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="load failed"/>';
35
- break;
36
- default:
37
- request.status_code_view = request.status_code;
38
- break;
15
+ const initiator = global.initiators[request.initiator_id] ?? null;
16
+ if (initiator !== null) { // the initiator may be missing if the clean list is executed in parrallel
17
+ request.loaded = false;
18
+ request.to_refresh = true;
19
+ if (request.pin == undefined) {
20
+ request.pin = "";
39
21
  }
40
- }
41
-
42
- global.requests[request_id] = request;
43
-
44
- if (!request.pin) {
45
- get_request(request_id);
22
+ if (request.filter == undefined) {
23
+ request.filter = "---";
24
+ }
25
+ request.session_id = session_id;
26
+ request.initiator = initiator;
27
+
28
+ if (request.in_progress) {
29
+ request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$" alt="loading"/>';
30
+ } else {
31
+ switch (request.status_code) {
32
+ case 0:
33
+ request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="loading"/>';
34
+ break;
35
+ case -1:
36
+ request.status_code_view = '<img class="icon" src="static/icons/math-multiplication-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="load failed"/>';
37
+ break;
38
+ default:
39
+ request.status_code_view = request.status_code;
40
+ break;
41
+ }
42
+ }
43
+
44
+ global.requests[request_id] = request;
45
+
46
+ if (!request.pin) {
47
+ get_request(request_id);
48
+ }
46
49
  }
47
50
  }
48
51