httpdbg 1.1.1__tar.gz → 1.2.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 (93) hide show
  1. {httpdbg-1.1.1 → httpdbg-1.2.0}/PKG-INFO +6 -6
  2. {httpdbg-1.1.1 → httpdbg-1.2.0}/README.md +5 -5
  3. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/__init__.py +1 -1
  4. httpdbg-1.2.0/httpdbg/env.py +4 -0
  5. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/all.py +19 -10
  6. httpdbg-1.2.0/httpdbg/hooks/fastapi.py +156 -0
  7. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/flask.py +29 -9
  8. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/pytest.py +4 -2
  9. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/socket.py +66 -40
  10. httpdbg-1.2.0/httpdbg/hooks/starlette.py +58 -0
  11. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/unittest.py +3 -3
  12. httpdbg-1.2.0/httpdbg/hooks/uvicorn.py +129 -0
  13. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/initiator.py +45 -72
  14. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/records.py +30 -11
  15. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/api.py +1 -1
  16. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg.egg-info/PKG-INFO +6 -6
  17. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg.egg-info/SOURCES.txt +6 -2
  18. httpdbg-1.2.0/tests/test_server_hook_fastapi.py +115 -0
  19. httpdbg-1.1.1/tests/test_hook_flask.py → httpdbg-1.2.0/tests/test_server_hook_flask.py +8 -2
  20. httpdbg-1.1.1/httpdbg/env.py +0 -7
  21. {httpdbg-1.1.1 → httpdbg-1.2.0}/LICENSE +0 -0
  22. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/__main__.py +0 -0
  23. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/args.py +0 -0
  24. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/exception.py +0 -0
  25. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/__init__.py +0 -0
  26. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/aiohttp.py +0 -0
  27. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/external.py +0 -0
  28. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/generic.py +0 -0
  29. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/http.py +0 -0
  30. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/httpx.py +0 -0
  31. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/requests.py +0 -0
  32. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/template_sitecustomize.py +0 -0
  33. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/urllib3.py +0 -0
  34. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/hooks/utils.py +0 -0
  35. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/log.py +0 -0
  36. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/mode_console.py +0 -0
  37. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/mode_module.py +0 -0
  38. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/mode_script.py +0 -0
  39. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/preview.py +0 -0
  40. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/server.py +0 -0
  41. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/utils.py +0 -0
  42. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/__init__.py +0 -0
  43. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/app.py +0 -0
  44. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/api.js +0 -0
  45. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/column.css +0 -0
  46. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/configuration.css +0 -0
  47. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/configuration.js +0 -0
  48. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/detail.css +0 -0
  49. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/favicon.ico +0 -0
  50. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/filter.js +0 -0
  51. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  52. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  53. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  54. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  55. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  56. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  57. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  58. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  59. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  60. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  61. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  62. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  63. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  64. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  65. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  66. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/index.htm +0 -0
  67. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/main.js +0 -0
  68. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  69. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/render.js +0 -0
  70. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/style.css +0 -0
  71. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg/webapp/static/table.css +0 -0
  72. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  73. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg.egg-info/entry_points.txt +0 -0
  74. {httpdbg-1.1.1 → httpdbg-1.2.0}/httpdbg.egg-info/top_level.txt +0 -0
  75. {httpdbg-1.1.1 → httpdbg-1.2.0}/pyproject.toml +0 -0
  76. {httpdbg-1.1.1 → httpdbg-1.2.0}/setup.cfg +0 -0
  77. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_api.py +0 -0
  78. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_cli.py +0 -0
  79. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_context_manager.py +0 -0
  80. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_aiohttp.py +0 -0
  81. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_httpx.py +0 -0
  82. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_pytest.py +0 -0
  83. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_requests.py +0 -0
  84. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_unittest.py +0 -0
  85. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_hook_urllib3.py +0 -0
  86. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_initiator.py +0 -0
  87. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_mode_console.py +0 -0
  88. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_mode_module_pytest.py +0 -0
  89. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_mode_script.py +0 -0
  90. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_multiprocess.py +0 -0
  91. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_preview.py +0 -0
  92. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_pyhttpdbg_server.py +0 -0
  93. {httpdbg-1.1.1 → httpdbg-1.2.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 1.1.1
3
+ Version: 1.2.0
4
4
  Summary: A very simple tool to debug HTTP(S) client and server requests.
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License: Apache-2.0
@@ -114,7 +114,6 @@ Press CTRL+C to quit
114
114
 
115
115
  ![httpdbg 1.1.0 - pyhttpdbg -m flask --app demoflask run](https://github.com/cle-b/httpdbg/blob/main/ui_server.png?raw=true)
116
116
 
117
- This feature is new and not fully implemented. See [limitations](#limitations) for more details.
118
117
 
119
118
  ## initiators and groups
120
119
 
@@ -146,7 +145,8 @@ The initiator is the low-level socket method that receives the data. The group i
146
145
 
147
146
  | packages | status | initiator | group |
148
147
  |----------------|-------------------------------------|-----------|-----------|
149
- | flask | supported | no | yes |
148
+ | flask | supported | - | yes |
149
+ | fastapi | supported | - | yes |
150
150
 
151
151
  ### supported test framework
152
152
 
@@ -154,8 +154,8 @@ The requests are grouped by tests.
154
154
 
155
155
  | packages | status | initiator | group |
156
156
  |----------------|-------------------------------------|-----------|-----------|
157
- | pytest | supported | no | yes |
158
- | unittest | supported | no | yes |
157
+ | pytest | supported | - | yes |
158
+ | unittest | supported | - | yes |
159
159
 
160
160
  ## configuration
161
161
 
@@ -218,7 +218,7 @@ The requests:
218
218
 
219
219
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
220
220
 
221
- Support for recording requests on the server side is new in `httpdbg 1.0.0` and is currently limited. For example, it doesn't work with `FastAPI`, which uses `uvloop`, but it does work with `Uvicorn` if the loop is `asyncio`. Work is in progress to improve this.
221
+ Support for recording requests on the server side is new in `httpdbg 1.0.0`. In some cases, like with |FastAPI| using |uvloop|, we had to implement a new mechanism to make request recording possible. This is still a work in progress as we aim to support more frameworks.
222
222
 
223
223
  ## documentation
224
224
 
@@ -87,7 +87,6 @@ Press CTRL+C to quit
87
87
 
88
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
89
 
90
- This feature is new and not fully implemented. See [limitations](#limitations) for more details.
91
90
 
92
91
  ## initiators and groups
93
92
 
@@ -119,7 +118,8 @@ The initiator is the low-level socket method that receives the data. The group i
119
118
 
120
119
  | packages | status | initiator | group |
121
120
  |----------------|-------------------------------------|-----------|-----------|
122
- | flask | supported | no | yes |
121
+ | flask | supported | - | yes |
122
+ | fastapi | supported | - | yes |
123
123
 
124
124
  ### supported test framework
125
125
 
@@ -127,8 +127,8 @@ The requests are grouped by tests.
127
127
 
128
128
  | packages | status | initiator | group |
129
129
  |----------------|-------------------------------------|-----------|-----------|
130
- | pytest | supported | no | yes |
131
- | unittest | supported | no | yes |
130
+ | pytest | supported | - | yes |
131
+ | unittest | supported | - | yes |
132
132
 
133
133
  ## configuration
134
134
 
@@ -191,7 +191,7 @@ The requests:
191
191
 
192
192
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
193
193
 
194
- Support for recording requests on the server side is new in `httpdbg 1.0.0` and is currently limited. For example, it doesn't work with `FastAPI`, which uses `uvloop`, but it does work with `Uvicorn` if the loop is `asyncio`. Work is in progress to improve this.
194
+ Support for recording requests on the server side is new in `httpdbg 1.0.0`. In some cases, like with |FastAPI| using |uvloop|, we had to implement a new mechanism to make request recording possible. This is still a work in progress as we aim to support more frameworks.
195
195
 
196
196
  ## documentation
197
197
 
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "1.1.1"
6
+ __version__ = "1.2.0"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -0,0 +1,4 @@
1
+ HTTPDBG_MULTIPROCESS_DIR = "HTTPDBG_MULTIPROCESS_DIR"
2
+
3
+ HTTPDBG_LOG_LEVEL = "HTTPDBG_LOG_LEVEL"
4
+ HTTPDBG_LOG_PATH = "HTTPDBG_LOG_PATH"
@@ -7,15 +7,19 @@ from typing import Union
7
7
 
8
8
  from httpdbg.hooks.aiohttp import hook_aiohttp
9
9
  from httpdbg.hooks.external import watcher_external
10
+ from httpdbg.hooks.fastapi import hook_fastapi
11
+ from httpdbg.hooks.flask import hook_flask
10
12
  from httpdbg.hooks.generic import hook_generic
11
13
  from httpdbg.hooks.http import hook_http
12
14
  from httpdbg.hooks.httpx import hook_httpx
13
15
  from httpdbg.hooks.pytest import hook_pytest
14
16
  from httpdbg.hooks.requests import hook_requests
15
17
  from httpdbg.hooks.socket import hook_socket
18
+ from httpdbg.hooks.starlette import hook_starlette
16
19
  from httpdbg.hooks.unittest import hook_unittest
17
20
  from httpdbg.hooks.urllib3 import hook_urllib3
18
- from httpdbg.hooks.flask import hook_flask
21
+ from httpdbg.hooks.uvicorn import hook_uvicorn
22
+
19
23
  from httpdbg.records import HTTPRecords
20
24
 
21
25
 
@@ -33,12 +37,17 @@ def httprecord(
33
37
  with watcher_external(records, initiators, server):
34
38
  with hook_flask(records):
35
39
  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
40
+ with hook_fastapi(records):
41
+ with hook_starlette(records):
42
+ with hook_uvicorn(records):
43
+ with hook_http(records):
44
+ with hook_httpx(records):
45
+ with hook_requests(records):
46
+ with hook_urllib3(records):
47
+ with hook_aiohttp(records):
48
+ with hook_pytest(records):
49
+ with hook_unittest(records):
50
+ with hook_generic(
51
+ records, initiators
52
+ ):
53
+ yield records
@@ -0,0 +1,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
3
+ from contextlib import contextmanager
4
+ import functools
5
+ from functools import wraps
6
+ from typing import Dict
7
+ from typing import Generator
8
+ from typing import Union
9
+
10
+ import datetime
11
+
12
+ from httpdbg.hooks.utils import decorate
13
+ from httpdbg.hooks.utils import getcallargs
14
+ from httpdbg.hooks.utils import undecorate
15
+ from httpdbg.initiator import httpdbg_endpoint
16
+ from httpdbg.records import HTTPRecords
17
+
18
+
19
+ # decorate the function decorated by @app.get; @app.post etc.
20
+ def set_hook_fastapi_endpoint(records: HTTPRecords, method: Callable):
21
+
22
+ @wraps(method)
23
+ def hook(*args, **kwargs):
24
+
25
+ with httpdbg_endpoint(
26
+ records,
27
+ method,
28
+ *args,
29
+ **kwargs,
30
+ ):
31
+ ret = method(*args, **kwargs)
32
+ return ret
33
+
34
+ return hook
35
+
36
+
37
+ def set_hook_fastapi_apirouter_add_api_route(
38
+ records: HTTPRecords,
39
+ method: Callable,
40
+ already_mapped: Union[Dict[Callable, Callable], None] = None,
41
+ ):
42
+
43
+ @wraps(method)
44
+ def hook(*args, **kwargs):
45
+ callargs = getcallargs(method, *args, **kwargs)
46
+
47
+ param = "endpoint"
48
+
49
+ if param in callargs:
50
+ original_view_func = callargs[param]
51
+ if original_view_func not in already_mapped:
52
+ callargs[param] = decorate(
53
+ records, callargs[param], set_hook_fastapi_endpoint
54
+ )
55
+ already_mapped[original_view_func] = callargs[param]
56
+ else:
57
+ callargs[param] = already_mapped[original_view_func]
58
+
59
+ args = [callargs[param] if x == original_view_func else x for x in args]
60
+ if param in kwargs:
61
+ kwargs[param] = callargs[param]
62
+
63
+ return method(*args, **kwargs)
64
+
65
+ return hook
66
+
67
+
68
+ # decorate the "app" returned by fastapi.routing.get_request_handler to link the socketdata and the endpoint group
69
+ def set_hook_fastapi_app(records: HTTPRecords, method: Callable):
70
+
71
+ @wraps(method)
72
+ async def hook(*args, **kwargs):
73
+
74
+ socketdata_id = None
75
+
76
+ callargs = getcallargs(method, *args, **kwargs)
77
+
78
+ if "request" in callargs:
79
+ if hasattr(callargs["request"], "__httpdbg_socketdata_id"):
80
+ socketdata_id = callargs["request"].__httpdbg_socketdata_id
81
+
82
+ with httpdbg_endpoint(
83
+ records,
84
+ method,
85
+ *args,
86
+ **kwargs,
87
+ ) as group:
88
+ if group and (group.id in records.groups):
89
+ if socketdata_id and (socketdata_id in records._sockets):
90
+ records.groups[group.id].tbegin = records._sockets[
91
+ socketdata_id
92
+ ].record.tbegin - datetime.timedelta(milliseconds=1)
93
+ records._sockets[socketdata_id].record.group_id = group.id
94
+
95
+ ret = await method(*args, **kwargs)
96
+
97
+ return ret
98
+
99
+ return hook
100
+
101
+
102
+ def set_hook_fastapi_routing_get_request_handler(
103
+ records: HTTPRecords, method: Callable
104
+ ):
105
+
106
+ @wraps(method)
107
+ def hook(*args, **kwargs):
108
+
109
+ app = method(*args, **kwargs)
110
+
111
+ app = decorate(records, app, set_hook_fastapi_app)
112
+
113
+ return app
114
+
115
+ return hook
116
+
117
+
118
+ @contextmanager
119
+ def hook_fastapi(records: HTTPRecords) -> Generator[None, None, None]:
120
+ hooks = False
121
+ try:
122
+ import fastapi.routing
123
+
124
+ already_mapped: Dict[Callable, Callable] = {}
125
+
126
+ set_hook_fastapi_apirouter_add_api_route_with_already_mapped = (
127
+ functools.partial(
128
+ set_hook_fastapi_apirouter_add_api_route, already_mapped=already_mapped
129
+ )
130
+ )
131
+
132
+ fastapi.routing.APIRouter.add_api_route = decorate(
133
+ records,
134
+ fastapi.routing.APIRouter.add_api_route,
135
+ set_hook_fastapi_apirouter_add_api_route_with_already_mapped,
136
+ )
137
+
138
+ fastapi.routing.get_request_handler = decorate(
139
+ records,
140
+ fastapi.routing.get_request_handler,
141
+ set_hook_fastapi_routing_get_request_handler,
142
+ )
143
+
144
+ hooks = True
145
+ except ImportError:
146
+ pass
147
+
148
+ yield
149
+
150
+ if hooks:
151
+ fastapi.routing.APIRouter.add_api_route = undecorate(
152
+ fastapi.routing.APIRouter.add_api_route
153
+ )
154
+ fastapi.routing.get_request_handler = undecorate(
155
+ fastapi.routing.get_request_handler
156
+ )
@@ -1,8 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from collections.abc import Callable
3
3
  from contextlib import contextmanager
4
+ import functools
4
5
  from functools import wraps
6
+ from typing import Dict
5
7
  from typing import Generator
8
+ from typing import Union
6
9
 
7
10
  from httpdbg.hooks.utils import getcallargs
8
11
  from httpdbg.hooks.utils import decorate
@@ -30,12 +33,11 @@ def set_hook_flask_endpoint(records: HTTPRecords, method: Callable):
30
33
  return hook
31
34
 
32
35
 
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):
36
+ def set_hook_flask_add_url_rule(
37
+ records: HTTPRecords,
38
+ method: Callable,
39
+ already_mapped: Union[Dict[Callable, Callable], None] = None,
40
+ ):
39
41
 
40
42
  def hook(*args, **kwargs):
41
43
 
@@ -62,7 +64,11 @@ def set_hook_flask_add_url_rule(records: HTTPRecords, method: Callable):
62
64
  return hook
63
65
 
64
66
 
65
- def set_hook_flask_register_error_handler(records: HTTPRecords, method: Callable):
67
+ def set_hook_flask_register_error_handler(
68
+ records: HTTPRecords,
69
+ method: Callable,
70
+ already_mapped: Union[Dict[Callable, Callable], None] = None,
71
+ ):
66
72
 
67
73
  def hook(*args, **kwargs):
68
74
 
@@ -95,14 +101,28 @@ def hook_flask(records: HTTPRecords) -> Generator[None, None, None]:
95
101
  try:
96
102
  import flask
97
103
 
104
+ # we must not apply the hook more than once on a mapped endpoint function
105
+ # AssertionError: View function mapping is overwriting an existing endpoint function: xxxx
106
+ already_mapped: Dict[Callable, Callable] = {}
107
+
108
+ set_hook_flask_add_url_rule_with_already_mapped = functools.partial(
109
+ set_hook_flask_add_url_rule, already_mapped=already_mapped
110
+ )
111
+
98
112
  flask.Flask.add_url_rule = decorate(
99
- records, flask.Flask.add_url_rule, set_hook_flask_add_url_rule
113
+ records,
114
+ flask.Flask.add_url_rule,
115
+ set_hook_flask_add_url_rule_with_already_mapped,
116
+ )
117
+
118
+ set_hook_flask_register_error_handler_with_already_mapped = functools.partial(
119
+ set_hook_flask_register_error_handler, already_mapped=already_mapped
100
120
  )
101
121
 
102
122
  flask.Flask.register_error_handler = decorate(
103
123
  records,
104
124
  flask.Flask.register_error_handler,
105
- set_hook_flask_register_error_handler,
125
+ set_hook_flask_register_error_handler_with_already_mapped,
106
126
  )
107
127
  flask.Flask.handle_exception = decorate(
108
128
  records, flask.Flask.handle_exception, set_hook_flask_endpoint
@@ -12,13 +12,15 @@ from httpdbg.log import logger
12
12
  from httpdbg.records import HTTPRecords
13
13
 
14
14
 
15
- def set_hook_for_pytest_fixture(_: HTTPRecords, method: Callable):
15
+ def set_hook_for_pytest_fixture(records: HTTPRecords, method: Callable):
16
16
  def hook(*args, **kwargs):
17
17
  logger().info("SET_HOOK_FOR_PYTEST_FIXTURE")
18
18
  callargs = getcallargs(method, *args, **kwargs)
19
19
 
20
20
  if "fixturefunc" in callargs:
21
- with httpdbg_tag(getattr(callargs["fixturefunc"], "__name__", "fixture")):
21
+ with httpdbg_tag(
22
+ records, getattr(callargs["fixturefunc"], "__name__", "fixture")
23
+ ):
22
24
  return method(*args, **kwargs)
23
25
  else:
24
26
  return method(*args, **kwargs)
@@ -186,46 +186,53 @@ def set_hook_for_socket_recv_into(records: HTTPRecords, method: Callable):
186
186
 
187
187
  nbytes = method(self, buffer, *args, **kwargs)
188
188
 
189
- if socketdata:
190
- if socketdata.record:
191
- logger().info(
192
- f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
193
- )
194
- socketdata.record.receive_data(buffer[:nbytes])
195
- else:
196
- socketdata.rawdata += buffer[:nbytes]
197
- http_detected = socketdata.http_detected()
198
- if http_detected:
199
- logger().info("RECV_INTO - http detected")
189
+ if buffer: # it appears that the buffer may be None (observed on Windows).
190
+ if socketdata:
191
+ if socketdata.record:
200
192
  logger().info(
201
193
  f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
202
194
  )
203
- with httpdbg_initiator(
204
- records,
205
- traceback.extract_stack(),
206
- method,
207
- self,
208
- buffer,
209
- *args,
210
- **kwargs,
211
- ) as initiator_and_group:
212
- initiator, group, is_new = initiator_and_group
213
- if is_new:
214
- tbegin = socketdata.tbegin - datetime.timedelta(
215
- milliseconds=1
216
- )
217
- initiator.tbegin = tbegin
218
- group.tbegin = tbegin
219
- socketdata.record = HTTPRecord(
220
- tbegin=socketdata.tbegin, is_client=False
195
+ socketdata.record.receive_data(buffer[:nbytes])
196
+ else:
197
+ socketdata.rawdata += buffer[:nbytes]
198
+ http_detected = socketdata.http_detected()
199
+ if http_detected:
200
+ logger().info("RECV_INTO - http detected")
201
+ logger().info(
202
+ f"RECV_INTO (after) - id={id(self)} buffer={(b''+buffer)[:20]}"
221
203
  )
222
- socketdata.record.address = socketdata.address
223
- socketdata.record.ssl = socketdata.ssl
224
- socketdata.record.receive_data(socketdata.rawdata)
225
- if records.server:
226
- records.requests[socketdata.record.id] = socketdata.record
227
- elif http_detected is False: # if None, there is nothing to do
228
- records._sockets[id(self)] = None
204
+ with httpdbg_initiator(
205
+ records,
206
+ traceback.extract_stack(),
207
+ method,
208
+ self,
209
+ buffer,
210
+ *args,
211
+ **kwargs,
212
+ ) as initiator_and_group:
213
+ initiator, group, is_new = initiator_and_group
214
+ if is_new:
215
+ tbegin = socketdata.tbegin - datetime.timedelta(
216
+ milliseconds=1
217
+ )
218
+ initiator.tbegin = tbegin
219
+ group.tbegin = tbegin
220
+ socketdata.record = HTTPRecord(
221
+ records.current_initiator,
222
+ records.current_group,
223
+ records.current_tag,
224
+ tbegin=socketdata.tbegin,
225
+ is_client=False,
226
+ )
227
+ socketdata.record.address = socketdata.address
228
+ socketdata.record.ssl = socketdata.ssl
229
+ socketdata.record.receive_data(socketdata.rawdata)
230
+ if records.server:
231
+ records.requests[socketdata.record.id] = (
232
+ socketdata.record
233
+ )
234
+ elif http_detected is False: # if None, there is nothing to do
235
+ records._sockets[id(self)] = None
229
236
 
230
237
  return nbytes
231
238
 
@@ -270,7 +277,11 @@ def set_hook_for_socket_recv(records: HTTPRecords, method: Callable):
270
277
  initiator.tbegin = tbegin
271
278
  group.tbegin = tbegin
272
279
  socketdata.record = HTTPRecord(
273
- tbegin=socketdata.tbegin, is_client=False
280
+ records.current_initiator,
281
+ records.current_group,
282
+ records.current_tag,
283
+ tbegin=socketdata.tbegin,
284
+ is_client=False,
274
285
  )
275
286
  socketdata.record.address = socketdata.address
276
287
  socketdata.record.ssl = socketdata.ssl
@@ -320,7 +331,12 @@ def set_hook_for_socket_sendall(records: HTTPRecords, method: Callable):
320
331
  )
321
332
  initiator.tbegin = tbegin
322
333
  group.tbegin = tbegin
323
- socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
334
+ socketdata.record = HTTPRecord(
335
+ records.current_initiator,
336
+ records.current_group,
337
+ records.current_tag,
338
+ tbegin=socketdata.tbegin,
339
+ )
324
340
  socketdata.record.address = socketdata.address
325
341
  socketdata.record.ssl = socketdata.ssl
326
342
  socketdata.record.send_data(socketdata.rawdata)
@@ -371,7 +387,12 @@ def set_hook_for_socket_send(records: HTTPRecords, method: Callable):
371
387
  )
372
388
  initiator.tbegin = tbegin
373
389
  group.tbegin = tbegin
374
- socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
390
+ socketdata.record = HTTPRecord(
391
+ records.current_initiator,
392
+ records.current_group,
393
+ records.current_tag,
394
+ tbegin=socketdata.tbegin,
395
+ )
375
396
  socketdata.record.address = socketdata.address
376
397
  socketdata.record.ssl = socketdata.ssl
377
398
  socketdata.record.send_data(socketdata.rawdata)
@@ -446,7 +467,12 @@ def set_hook_for_sslobject_write(records: HTTPRecords, method: Callable):
446
467
  )
447
468
  initiator.tbegin = tbegin
448
469
  group.tbegin = tbegin
449
- socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
470
+ socketdata.record = HTTPRecord(
471
+ records.current_initiator,
472
+ records.current_group,
473
+ records.current_tag,
474
+ tbegin=socketdata.tbegin,
475
+ )
450
476
  socketdata.record.address = socketdata.address
451
477
  socketdata.record.ssl = socketdata.ssl
452
478
  socketdata.record.send_data(socketdata.rawdata)
@@ -0,0 +1,58 @@
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 decorate
8
+ from httpdbg.hooks.utils import getcallargs
9
+ from httpdbg.hooks.utils import undecorate
10
+ from httpdbg.records import HTTPRecords
11
+
12
+
13
+ def set_hook_starlette_request_init(records: HTTPRecords, method: Callable):
14
+
15
+ @wraps(method)
16
+ def hook(self, *args, **kwargs):
17
+
18
+ callargs = getcallargs(method, self, *args, **kwargs)
19
+
20
+ if "receive" in callargs:
21
+ if hasattr(callargs["receive"], "__self__"):
22
+ if hasattr(callargs["receive"].__self__, "transport"):
23
+ if hasattr(
24
+ callargs["receive"].__self__.transport,
25
+ "_TCPTransport__httpdbg_socketdata",
26
+ ):
27
+ # make the link between starlette.requests.Request (self) and uvicorn.protocols.http.httptools_impl.HttpToolsProtocol (socketdata)
28
+ self.__httpdbg_socketdata_id = callargs[
29
+ "receive"
30
+ ].__self__.transport._TCPTransport__httpdbg_socketdata.id
31
+
32
+ return method(self, *args, **kwargs)
33
+
34
+ return hook
35
+
36
+
37
+ @contextmanager
38
+ def hook_starlette(records: HTTPRecords) -> Generator[None, None, None]:
39
+ hooks = False
40
+ try:
41
+ import starlette.requests
42
+
43
+ starlette.requests.Request.__init__ = decorate(
44
+ records,
45
+ starlette.requests.Request.__init__,
46
+ set_hook_starlette_request_init,
47
+ )
48
+
49
+ hooks = True
50
+ except ImportError:
51
+ pass
52
+
53
+ yield
54
+
55
+ if hooks:
56
+ starlette.requests.Request.__init__ = undecorate(
57
+ starlette.requests.Request.__init__
58
+ )
@@ -45,15 +45,15 @@ def set_hook_for_unittest_run(records: HTTPRecords, method: Callable):
45
45
  return hook
46
46
 
47
47
 
48
- def set_hook_for_unittest_fixture(_: HTTPRecords, method: Callable):
48
+ def set_hook_for_unittest_fixture(records: HTTPRecords, method: Callable):
49
49
 
50
50
  def hook(*args, **kwargs):
51
51
 
52
52
  if getattr(method, "__name__") == "_callSetUp":
53
- with httpdbg_tag("setUp"):
53
+ with httpdbg_tag(records, "setUp"):
54
54
  return method(*args, **kwargs)
55
55
  elif getattr(method, "__name__") == "_callTearDown":
56
- with httpdbg_tag("tearDown"):
56
+ with httpdbg_tag(records, "tearDown"):
57
57
  return method(*args, **kwargs)
58
58
  else:
59
59
  return method(*args, **kwargs)