httpdbg 2.0.1__tar.gz → 2.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 (101) hide show
  1. {httpdbg-2.0.1 → httpdbg-2.1.0}/PKG-INFO +7 -1
  2. {httpdbg-2.0.1 → httpdbg-2.1.0}/README.md +6 -0
  3. httpdbg-2.1.0/httpdbg/__init__.py +20 -0
  4. httpdbg-2.1.0/httpdbg/__main__.py +93 -0
  5. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/args.py +9 -3
  6. httpdbg-2.1.0/httpdbg/export.py +109 -0
  7. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/initiator.py +55 -47
  8. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/app.py +19 -51
  9. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/api.js +34 -16
  10. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/index.htm +59 -46
  11. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg.egg-info/PKG-INFO +7 -1
  12. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg.egg-info/SOURCES.txt +1 -0
  13. httpdbg-2.0.1/httpdbg/__init__.py +0 -7
  14. httpdbg-2.0.1/httpdbg/__main__.py +0 -69
  15. {httpdbg-2.0.1 → httpdbg-2.1.0}/LICENSE +0 -0
  16. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/env.py +0 -0
  17. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/exception.py +0 -0
  18. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/__init__.py +0 -0
  19. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/aiohttp.py +0 -0
  20. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/all.py +0 -0
  21. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/external.py +0 -0
  22. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/fastapi.py +0 -0
  23. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/flask.py +0 -0
  24. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/generic.py +0 -0
  25. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/h2.py +0 -0
  26. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/http.py +0 -0
  27. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/httpx.py +0 -0
  28. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/pytest.py +0 -0
  29. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/record.py +0 -0
  30. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/recordhttp1.py +0 -0
  31. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/recordhttp2.py +0 -0
  32. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/requests.py +0 -0
  33. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/socket.py +0 -0
  34. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/starlette.py +0 -0
  35. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/template_sitecustomize.py +0 -0
  36. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/unittest.py +0 -0
  37. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/urllib3.py +0 -0
  38. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/utils.py +0 -0
  39. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/hooks/uvicorn.py +0 -0
  40. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/http_status_code.py +0 -0
  41. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/log.py +0 -0
  42. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/mode_console.py +0 -0
  43. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/mode_module.py +0 -0
  44. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/mode_script.py +0 -0
  45. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/preview.py +0 -0
  46. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/records.py +0 -0
  47. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/server.py +0 -0
  48. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/utils.py +0 -0
  49. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/__init__.py +0 -0
  50. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/api.py +0 -0
  51. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/column.css +0 -0
  52. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/configuration.css +0 -0
  53. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/configuration.js +0 -0
  54. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/detail.css +0 -0
  55. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/favicon.ico +0 -0
  56. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/filter.js +0 -0
  57. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  58. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  59. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  60. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  61. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  62. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  63. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  64. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  65. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  66. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  67. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  68. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  69. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  70. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  71. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  72. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/main.js +0 -0
  73. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  74. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/render.js +0 -0
  75. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/style.css +0 -0
  76. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg/webapp/static/table.css +0 -0
  77. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  78. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg.egg-info/entry_points.txt +0 -0
  79. {httpdbg-2.0.1 → httpdbg-2.1.0}/httpdbg.egg-info/top_level.txt +0 -0
  80. {httpdbg-2.0.1 → httpdbg-2.1.0}/pyproject.toml +0 -0
  81. {httpdbg-2.0.1 → httpdbg-2.1.0}/setup.cfg +0 -0
  82. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_api.py +0 -0
  83. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_cli.py +0 -0
  84. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_context_manager.py +0 -0
  85. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_aiohttp.py +0 -0
  86. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_h2.py +0 -0
  87. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_httpx.py +0 -0
  88. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_pytest.py +0 -0
  89. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_requests.py +0 -0
  90. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_unittest.py +0 -0
  91. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_hook_urllib3.py +0 -0
  92. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_initiator.py +0 -0
  93. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_mode_console.py +0 -0
  94. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_mode_module_pytest.py +0 -0
  95. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_mode_script.py +0 -0
  96. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_multiprocess.py +0 -0
  97. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_preview.py +0 -0
  98. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_pyhttpdbg_server.py +0 -0
  99. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_server.py +0 -0
  100. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_server_hook_fastapi.py +0 -0
  101. {httpdbg-2.0.1 → httpdbg-2.1.0}/tests/test_server_hook_flask.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 2.0.1
3
+ Version: 2.1.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
@@ -192,6 +192,8 @@ options:
192
192
  --only-client record only HTTP client requests
193
193
  --keep-up, -k keep the server up even if the requests have been read
194
194
  --force-quit, -q stop the server even if the requests have not been read
195
+ --export-html EXPORT_HTML
196
+ the path to the export file
195
197
  --console run a python console (default)
196
198
  --module MODULE, -m MODULE
197
199
  run library module as a script (the next args are passed to pytest as is)
@@ -225,6 +227,10 @@ The requests:
225
227
  * are still available in the web page even if the python process stopped (except if you force quit before the requests have been loaded by the web page).
226
228
  * are automatically cleaned if a new execution is detected.
227
229
 
230
+ ## export
231
+
232
+ You can export the HTTP traces in a single HTML file. In that case, the web interface is not available during the execution of the python command.
233
+
228
234
  ## limitations
229
235
 
230
236
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
@@ -166,6 +166,8 @@ options:
166
166
  --only-client record only HTTP client requests
167
167
  --keep-up, -k keep the server up even if the requests have been read
168
168
  --force-quit, -q stop the server even if the requests have not been read
169
+ --export-html EXPORT_HTML
170
+ the path to the export file
169
171
  --console run a python console (default)
170
172
  --module MODULE, -m MODULE
171
173
  run library module as a script (the next args are passed to pytest as is)
@@ -199,6 +201,10 @@ The requests:
199
201
  * are still available in the web page even if the python process stopped (except if you force quit before the requests have been loaded by the web page).
200
202
  * are automatically cleaned if a new execution is detected.
201
203
 
204
+ ## export
205
+
206
+ You can export the HTTP traces in a single HTML file. In that case, the web interface is not available during the execution of the python command.
207
+
202
208
  ## limitations
203
209
 
204
210
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
@@ -0,0 +1,20 @@
1
+ __version__ = "2.1.0"
2
+
3
+ __all__ = ["export_html", "httprecord", "HTTPRecords"]
4
+
5
+
6
+ # lazy loading to avoid circular import issue
7
+ def __getattr__(name):
8
+ if name == "export_html":
9
+ from httpdbg.export import export_html
10
+
11
+ return export_html
12
+ if name == "httprecord":
13
+ from httpdbg.hooks.all import httprecord
14
+
15
+ return httprecord
16
+ if name == "HTTPRecords":
17
+ from httpdbg.records import HTTPRecords
18
+
19
+ return HTTPRecords
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,93 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ import sys
4
+ import time
5
+ from typing import Union
6
+
7
+ from httpdbg import __version__
8
+ from httpdbg.args import read_args
9
+ from httpdbg.export import export_html
10
+ from httpdbg.hooks.all import httprecord
11
+ from httpdbg.log import set_env_for_logging
12
+ from httpdbg.server import httpdbg_srv
13
+ from httpdbg.mode_console import run_console
14
+ from httpdbg.mode_module import run_module
15
+ from httpdbg.mode_script import run_script
16
+ from httpdbg.records import HTTPRecords
17
+
18
+
19
+ def print_msg(msg):
20
+ sep = ".... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --."
21
+ msg = f"{sep}\n{msg}\n{sep}"
22
+ print(msg)
23
+
24
+
25
+ def pyhttpdbg(
26
+ params: argparse.Namespace, subparams: list[str], test_mode: bool = False
27
+ ):
28
+
29
+ set_env_for_logging(params.log_level, params.log)
30
+
31
+ url = f"http://{params.host}:{params.port}/{'?hi=on' if params.console else ''}"
32
+
33
+ if not params.no_banner:
34
+ print_msg(f" httpdbg - HTTP(S) requests available at {url}")
35
+
36
+ sys.path.insert(0, "") # to mimic the default python command behavior
37
+
38
+ def run_recorder(
39
+ records: HTTPRecords,
40
+ initiators: Union[list[str], None],
41
+ record_server: bool,
42
+ test_mode: bool,
43
+ ):
44
+ records.server = record_server
45
+ with httprecord(records, initiators, server=records.server):
46
+ if params.module:
47
+ run_module(subparams)
48
+ elif params.script:
49
+ run_script(subparams)
50
+ else:
51
+ run_console(records, test_mode)
52
+
53
+ if params.export_html:
54
+ records = HTTPRecords()
55
+
56
+ run_recorder(records, params.initiator, not params.only_client, test_mode)
57
+
58
+ export_html(records, Path(params.export_html))
59
+
60
+ else:
61
+ with httpdbg_srv(params.host, params.port) as records:
62
+
63
+ run_recorder(records, params.initiator, not params.only_client, test_mode)
64
+
65
+ if not (params.force_quit or test_mode):
66
+ if not params.no_banner:
67
+ print_msg(f" httpdbg - HTTP(S) requests available at {url}")
68
+
69
+ if params.keep_up:
70
+ input("Press enter to quit")
71
+ else:
72
+ # we keep the server up until all the requests have been loaded in the web interface
73
+ print(
74
+ "Waiting until all the requests have been loaded in the web interface."
75
+ )
76
+ print("Press Ctrl+C to quit.")
77
+ try:
78
+ while records.unread:
79
+ time.sleep(0.5)
80
+ except KeyboardInterrupt: # pragma: no cover
81
+ pass
82
+
83
+
84
+ def pyhttpdbg_entry_point(test_mode=False):
85
+ params, subparams = read_args(sys.argv[1:])
86
+ if params.version:
87
+ print(__version__)
88
+ else:
89
+ pyhttpdbg(params, subparams, test_mode=test_mode)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ pyhttpdbg_entry_point()
@@ -40,22 +40,28 @@ def read_args(args: list[str]) -> tuple[argparse.Namespace, list[str]]:
40
40
  help="record only HTTP client requests",
41
41
  )
42
42
 
43
- server_state = parser.add_mutually_exclusive_group()
43
+ server_or_export = parser.add_mutually_exclusive_group()
44
44
 
45
- server_state.add_argument(
45
+ server_or_export.add_argument(
46
46
  "--keep-up",
47
47
  "-k",
48
48
  action="store_true",
49
49
  help="keep the server up even if the requests have been read",
50
50
  )
51
51
 
52
- server_state.add_argument(
52
+ server_or_export.add_argument(
53
53
  "--force-quit",
54
54
  "-q",
55
55
  action="store_true",
56
56
  help="stop the server even if the requests have not been read",
57
57
  )
58
58
 
59
+ server_or_export.add_argument(
60
+ "--export-html",
61
+ type=str,
62
+ help="the path to the export file",
63
+ )
64
+
59
65
  parser.add_argument(
60
66
  "--log-level",
61
67
  type=LogLevel,
@@ -0,0 +1,109 @@
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+ import re
5
+ import xml.etree.ElementTree as ET
6
+
7
+ import httpdbg
8
+ from httpdbg import HTTPRecords
9
+ from httpdbg.webapp.api import RequestListPayload, RequestPayload
10
+
11
+
12
+ def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
13
+
14
+ current_dir = Path(__file__).resolve().parent
15
+
16
+ with open(Path(current_dir) / "webapp/static/index.htm") as findexhtml:
17
+ html = findexhtml.read()
18
+
19
+ html = html.replace("$**HTTPDBG_VERSION**$", httpdbg.__version__)
20
+
21
+ # favicon
22
+ with open(current_dir / "webapp/static/favicon.ico", "rb") as ffavicon:
23
+ b64_icon = base64.b64encode(ffavicon.read()).decode("utf-8")
24
+ data_uri = f"data:image/x-icon;base64,{b64_icon}"
25
+ html = html.replace(
26
+ '<link rel="shortcut icon" href="static/favicon.ico">',
27
+ f'<link rel="icon" type="image/x-icon" href="{data_uri}">',
28
+ )
29
+
30
+ # icons
31
+ def svg_file_to_symbol(svg_path: Path, symbol_id: str) -> str:
32
+ ET.register_namespace("", "http://www.w3.org/2000/svg")
33
+ root = ET.parse(svg_path).getroot()
34
+ symbol = ET.Element(
35
+ "symbol", {"id": symbol_id, "viewBox": root.attrib["viewBox"]}
36
+ )
37
+ for child in list(root):
38
+ symbol.append(child)
39
+ return ET.tostring(symbol, encoding="unicode")
40
+
41
+ icons_inline = ""
42
+ for icon_path in (current_dir / "webapp/static/icons").glob("*.svg"):
43
+ icons_inline += "\n " + svg_file_to_symbol(
44
+ icon_path, icon_path.name[:-4]
45
+ )
46
+ icons_inline = f'<svg width="0" height="0" style="position:absolute;visibility:hidden" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">{icons_inline}\n </svg>'
47
+ html = html.replace("$**PRELOAD_ICONS**$", icons_inline)
48
+
49
+ # css
50
+ stylesheet_pattern = (
51
+ r'<link\s+[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\']'
52
+ )
53
+ for stylesheet in re.findall(stylesheet_pattern, html):
54
+ with open(Path(current_dir) / "webapp" / stylesheet) as fcss:
55
+ html = html.replace(
56
+ f'<link rel="stylesheet" href="{stylesheet}">',
57
+ f"<style>{fcss.read()}</style>",
58
+ )
59
+
60
+ # js
61
+ javascript_pattern = r'<script\s+[^>]*src=["\']([^"\']+)["\']'
62
+ for js in re.findall(javascript_pattern, html):
63
+ with open(Path(current_dir) / "webapp" / js) as fjs:
64
+ html = html.replace(
65
+ f'<script src="{js}"></script>', f"<script>{fjs.read()}</script>"
66
+ )
67
+
68
+ # static export of the requests data
69
+ if for_export:
70
+ static_all_requests = json.dumps(
71
+ records, cls=RequestListPayload, ensure_ascii=False
72
+ )
73
+ map_requests: dict[str, object] = dict()
74
+ for record in records:
75
+ map_requests[record.id] = json.loads(json.dumps(record, cls=RequestPayload))
76
+ static_requests: str = json.dumps(map_requests, ensure_ascii=False)
77
+
78
+ def safe_for_script_tag(s: str) -> str:
79
+ return s.replace("</script>", "<\\/script>")
80
+
81
+ html_export = f"""
82
+ <script id="all-requests" type="application/json">
83
+ {safe_for_script_tag(static_all_requests)}
84
+ </script>
85
+
86
+ <script id="requests-map" type="application/json">
87
+ {safe_for_script_tag(static_requests)}
88
+ </script>
89
+
90
+ <script>
91
+ global.static_all_requests = JSON.parse(
92
+ document.getElementById('all-requests').textContent
93
+ );
94
+ global.static_requests = JSON.parse(
95
+ document.getElementById('requests-map').textContent
96
+ );
97
+ </script>
98
+ """
99
+
100
+ html = html.replace("$**EXPORT**$", html_export)
101
+ else:
102
+ html = html.replace("$**EXPORT**$", "")
103
+
104
+ return html
105
+
106
+
107
+ def export_html(records: HTTPRecords, filename: Path):
108
+ with open(filename, "w") as f:
109
+ f.write(generate_html(records))
@@ -2,7 +2,9 @@
2
2
  from collections.abc import Callable
3
3
  from contextlib import contextmanager
4
4
  import datetime
5
+ import functools
5
6
  import inspect
7
+ import linecache
6
8
  import os
7
9
  import platform
8
10
  import traceback
@@ -98,9 +100,9 @@ def in_lib(line: str, packages: list[str] = None):
98
100
  def get_current_instruction(
99
101
  extracted_stack: traceback.StackSummary,
100
102
  ) -> tuple[str, str, list[str]]:
101
- instruction = ""
102
- short_stack = ""
103
- stack = []
103
+ instruction: str = ""
104
+ short_stack: str = ""
105
+ stack: list[str] = []
104
106
 
105
107
  try:
106
108
  n_stack = -2
@@ -114,12 +116,7 @@ def get_current_instruction(
114
116
  n_stack -= 1
115
117
  framesummary = extracted_stack[n_stack - 1]
116
118
 
117
- short_stack = f'File "{framesummary.filename}", line {framesummary.lineno}, in {framesummary.name}\n'
118
119
  if os.path.exists(framesummary.filename) and framesummary.lineno is not None:
119
- instruction, s_stack = extract_short_stack_from_file(
120
- framesummary.filename, framesummary.lineno, 0, 8
121
- )
122
- short_stack += s_stack
123
120
 
124
121
  # stack
125
122
  to_include = False
@@ -131,14 +128,20 @@ def get_current_instruction(
131
128
  and ("importlib" not in fs.filename)
132
129
  ) # remove the stack before to start the user part
133
130
  if to_include:
134
- _, s_stack = extract_short_stack_from_file(
131
+ instruction, s_stack, l_stack = extract_short_stack_from_file(
135
132
  fs.filename,
136
133
  fs.lineno if fs.lineno else 0,
137
- 4 if last_stack else 0,
138
- 8,
139
134
  not last_stack,
140
135
  )
141
- stack.append(f'File "{fs.filename}", line {fs.lineno}, \n{s_stack}')
136
+ if last_stack:
137
+ short_stack = (
138
+ f'File "{fs.filename}", line {fs.lineno}, in {fs.name}\n'
139
+ )
140
+ short_stack += s_stack
141
+
142
+ stack.append(
143
+ f'File "{fs.filename}", line {fs.lineno}, \n{l_stack}\n'
144
+ )
142
145
 
143
146
  else:
144
147
  instruction = framesummary.line if framesummary.line else "console"
@@ -152,52 +155,57 @@ def get_current_instruction(
152
155
  return instruction.replace("\n", " "), short_stack, stack
153
156
 
154
157
 
158
+ @functools.cache
155
159
  def extract_short_stack_from_file(
156
160
  filename: str,
157
161
  lineno: int,
158
- before: int,
159
- after: int,
160
162
  stop_if_instruction_ends: bool = True,
161
- ) -> tuple[str, str]:
162
- instruction = ""
163
- short_stack = ""
163
+ ) -> tuple[str, str, str]:
164
+ before: int = 4
165
+ after: int = 8
166
+ instruction: str = ""
167
+ short_stack: str = ""
168
+ long_stack: str = ""
164
169
 
165
170
  try:
166
171
  if os.path.exists(filename):
167
- with open(filename, "r") as src:
168
- lines = src.read().splitlines()
169
-
170
- # copy the lines before
171
- for i in range(
172
- max(0, lineno - 1 - before), min(lineno - 1, len(lines))
173
- ):
174
- line = lines[i]
175
- short_stack += f" {i+1}. {line}\n"
176
-
177
- # try to recompose the instruction if on multi-lines
178
- end_of_instruction_found = False
179
- for i in range(max(0, lineno - 1), min(lineno - 1 + after, len(lines))):
180
- line = lines[i]
181
- if not end_of_instruction_found:
182
- instruction += line.strip()
183
- short_stack += f" {i+1}. {line}{' <====' if (before > 0 and i + 1 == lineno) else ''}\n"
184
- nb_parenthesis = 0
185
- for c in instruction[instruction.find("(") :]:
186
- if c == "(":
187
- nb_parenthesis += 1
188
- if c == ")":
189
- nb_parenthesis -= 1
190
- if nb_parenthesis == 0:
191
- end_of_instruction_found = True
192
- break
193
- if end_of_instruction_found and stop_if_instruction_ends:
172
+ # copy the lines before, only for the "long" stack (if they are not empty)
173
+ copyit = False
174
+ for i in range(max(0, lineno - before), lineno):
175
+ line = linecache.getline(filename, i).removesuffix("\n").rstrip()
176
+ copyit = copyit or (line != "")
177
+ if copyit:
178
+ long_stack += f" {i}. {line}\n"
179
+
180
+ # try to recompose the instruction if on multi-lines
181
+ end_of_instruction_found = False
182
+ for i in range(max(0, lineno), lineno + after):
183
+ line = linecache.getline(filename, i).removesuffix("\n").rstrip()
184
+ if not end_of_instruction_found:
185
+ instruction += line.strip()
186
+ short_stack += f" {i}. {line}\n"
187
+ long_stack += (
188
+ f" {i}. {line}{' <====' if (before > 0 and i == lineno) else ''}\n"
189
+ )
190
+ nb_parenthesis = 0
191
+ for c in instruction[instruction.find("(") :]:
192
+ if c == "(":
193
+ nb_parenthesis += 1
194
+ if c == ")":
195
+ nb_parenthesis -= 1
196
+ if nb_parenthesis == 0:
197
+ end_of_instruction_found = True
194
198
  break
199
+ if end_of_instruction_found and stop_if_instruction_ends:
200
+ break
195
201
  except Exception as ex:
202
+ tb = traceback.extract_tb(ex.__traceback__)
203
+ _, lineno, _, _ = tb[-1]
196
204
  logger().info(
197
- f"EXTRACT_SHORT_STACK_FROM_FILE {filename} lineno={lineno} before={before} after={after}- error - {str(ex)}"
205
+ f"EXTRACT_SHORT_STACK_FROM_FILE {filename} lineno={lineno} before={before} after={after}- error (line {lineno}) - {str(ex)}",
198
206
  )
199
207
 
200
- return instruction, short_stack
208
+ return instruction, short_stack, long_stack
201
209
 
202
210
 
203
211
  def construct_call_str(original_method, *args, **kwargs):
@@ -345,7 +353,7 @@ def httpdbg_endpoint(
345
353
  filename = inspect.getsourcefile(original_method)
346
354
  if filename:
347
355
  _, lineno = inspect.getsourcelines(original_method)
348
- instruction, s_stack = extract_short_stack_from_file(filename, lineno, 0, 8)
356
+ instruction, s_stack, _ = extract_short_stack_from_file(filename, lineno)
349
357
  full_label = (
350
358
  f'File "{filename}", line {lineno}, in {original_method.__name__}\n'
351
359
  )
@@ -1,18 +1,13 @@
1
- # -*- coding: utf-8 -*-
2
1
  from collections.abc import Callable
3
2
  from contextlib import contextmanager
4
- import glob
5
3
  from http.server import BaseHTTPRequestHandler
6
4
  import json
7
- import mimetypes
8
- import os
5
+ from pathlib import Path
9
6
  import re
10
7
  from urllib.parse import ParseResult
11
8
  from urllib.parse import parse_qs
12
9
  from urllib.parse import urlparse
13
10
 
14
-
15
- from httpdbg import __version__
16
11
  from httpdbg.log import logger
17
12
  from httpdbg.webapp.api import RequestListPayload, RequestPayload
18
13
  from httpdbg.records import HTTPRecords
@@ -56,61 +51,34 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
56
51
  break
57
52
 
58
53
  def serve_static(self, url: ParseResult):
59
- base_path = os.path.dirname(os.path.realpath(__file__))
60
-
61
- if not (
62
- (url.path.lower() in ["/", "index.htm", "index.html"])
63
- or url.path.startswith("/static/")
64
- ):
65
- return False
66
54
 
67
55
  if url.path.lower() in ["/", "index.htm", "index.html"]:
68
- self.path = "/static/index.htm"
69
-
70
- # get real path
71
- self.path = self.path.split("-+-")[0]
72
-
73
- fullpath = os.path.realpath(os.path.join(base_path, self.path[1:]))
56
+ from httpdbg.export import generate_html
74
57
 
75
- if not fullpath.startswith(base_path):
76
- # if the file is not in the static directory, we don't serve it
77
- return self.serve_not_found(url)
78
-
79
- if not os.path.exists(fullpath):
80
- return self.serve_not_found(url)
81
- else:
82
58
  self.send_response(200)
83
- self.send_header(
84
- "content-type", mimetypes.types_map[os.path.splitext(fullpath)[1]]
85
- )
86
- if self.path == "/static/index.htm":
87
- self.send_header_no_cache()
88
- else:
89
- self.send_header_with_cache(604800)
59
+ self.send_header("content-type", "text/html")
60
+ self.send_header_no_cache()
90
61
  self.end_headers()
62
+ self.wfile.write(
63
+ generate_html(self.records, for_export=False).encode("utf-8")
64
+ )
65
+ return True
91
66
 
92
- with open(fullpath, "rb") as f:
93
- filecontent = f.read()
94
-
95
- if b"$**PRELOAD_ICONS**$" in filecontent:
96
- icons = ""
97
- for icon in glob.glob(
98
- os.path.realpath(base_path) + "/static/icons/*.svg"
99
- ):
100
- icon_path = icon.replace(os.path.realpath(base_path) + "/", "")
101
- icons += f" <link rel='preload' href='{icon_path}-+-$**HTTPDBG_VERSION**$' as='image' type='image/svg+xml' />\n"
67
+ if url.path.lower() == "favicon.ico":
68
+ self.send_response(200)
69
+ self.send_header("content-type", "image/x-icon")
70
+ self.send_header_no_cache()
71
+ self.end_headers()
102
72
 
103
- filecontent = filecontent.replace(
104
- b"$**PRELOAD_ICONS**$", icons.encode("utf-8")
105
- )
73
+ current_dir = Path(__file__).resolve().parent
106
74
 
107
- filecontent = filecontent.replace(
108
- b"$**HTTPDBG_VERSION**$", __version__.encode("utf-8")
109
- )
75
+ with open(Path(current_dir) / "static/favicon.ico") as f:
76
+ filecontent = f.read()
77
+ self.wfile.write(filecontent.encode("utf-8"))
110
78
 
111
- self.wfile.write(filecontent)
79
+ return True
112
80
 
113
- return True
81
+ return False
114
82
 
115
83
  def serve_requests(self, url: ParseResult):
116
84
  if not (url.path.lower() == "/requests"):
@@ -5,7 +5,7 @@ const global = {
5
5
  sessions: {},
6
6
  requests: {},
7
7
  initiators: {},
8
- groups: {},
8
+ groups: {},
9
9
  connected: false,
10
10
  group_collapse: [],
11
11
  groupby: "default"
@@ -24,32 +24,35 @@ function save_request(request_id, request, session_id) {
24
24
  }
25
25
  request.session_id = session_id;
26
26
  request.initiator = initiator;
27
-
27
+
28
28
  if (request.in_progress) {
29
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$" alt="loading"/>';
29
+ request.status_code_view = '<svg class="icon" alt="loading"><use href="#wait-sandclock-icon"></use></svg>';
30
30
  } else {
31
31
  switch (request.status_code) {
32
32
  case 0:
33
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="loading"/>';
33
+ request.status_code_view = '<svg class="icon" alt="loading"><use href="#wait-sandclock-icon"></use></svg>';
34
34
  break;
35
35
  case -1:
36
- request.status_code_view = '<img class="icon" src="static/icons/math-multiplication-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="load failed"/>';
36
+ request.status_code_view = '<svg class="icon" alt="load failed"><use href="#math-multiplication-icon"></use></svg>';
37
37
  break;
38
38
  default:
39
39
  request.status_code_view = request.status_code;
40
40
  break;
41
41
  }
42
42
  }
43
-
44
- global.requests[request_id] = request;
45
-
43
+
44
+ global.requests[request_id] = request;
45
+
46
46
  if (!request.pin) {
47
47
  get_request(request_id);
48
- }
48
+ }
49
49
  }
50
50
  }
51
51
 
52
- async function get_all_requests() {
52
+ async function load_all_requests() {
53
+ if (typeof global.static_all_requests !== "undefined") {
54
+ return global.static_all_requests;
55
+ }
53
56
 
54
57
  var requests_already_loaded = 0
55
58
  for (const [request_id, request] of Object.entries(global.requests)) {
@@ -62,15 +65,22 @@ async function get_all_requests() {
62
65
  "requests_already_loaded": requests_already_loaded,
63
66
  })
64
67
 
65
- await fetch(url)
66
- .then(res => res.json())
68
+ const res = await fetch(url);
69
+ const data = await res.json();
70
+ return data;
71
+ }
72
+
73
+
74
+ async function get_all_requests() {
75
+
76
+ await load_all_requests()
67
77
  .then(data => {
68
78
  global.connected = true;
69
79
 
70
80
  if (data.session.id != global.session) {
71
- clean();
81
+ clean();
72
82
  global.session = data.session.id;
73
- global.sessions[data.session.id] = data.session;
83
+ global.sessions[data.session.id] = data.session;
74
84
  };
75
85
 
76
86
  // for the initiators and the groups, we can just save them without any verification
@@ -93,13 +103,21 @@ async function get_all_requests() {
93
103
  .catch((error) => {
94
104
  global.connected = false;
95
105
  });
106
+ }
107
+
108
+ async function load_request(request_id) {
109
+ if (typeof global.static_requests !== "undefined") {
110
+ return global.static_requests[request_id];
111
+ }
96
112
 
113
+ const res = await fetch("/request/" + request_id);
114
+ const data = await res.json();
115
+ return data;
97
116
  }
98
117
 
99
118
  async function get_request(request_id) {
100
119
 
101
- await fetch("/request/" + request_id)
102
- .then(res => res.json())
120
+ await load_request(request_id)
103
121
  .then(data => {
104
122
  global.connected = true;
105
123
  global.requests[request_id].filter = prepare_for_filter(global.requests[request_id].url);