httpx-debug 0.1.0__py3-none-any.whl
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.
- httpx_debug/__init__.py +14 -0
- httpx_debug/_clipboard.py +81 -0
- httpx_debug/_curl.py +78 -0
- httpx_debug/_hooks.py +149 -0
- httpx_debug-0.1.0.dist-info/METADATA +148 -0
- httpx_debug-0.1.0.dist-info/RECORD +8 -0
- httpx_debug-0.1.0.dist-info/WHEEL +4 -0
- httpx_debug-0.1.0.dist-info/licenses/LICENSE +21 -0
httpx_debug/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from httpx_debug._clipboard import copy
|
|
2
|
+
from httpx_debug._curl import DEFAULT_REDACT_HEADERS, REDACTED, to_curl
|
|
3
|
+
from httpx_debug._hooks import AsyncClient, Client, attach, detach
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"to_curl",
|
|
7
|
+
"copy",
|
|
8
|
+
"attach",
|
|
9
|
+
"detach",
|
|
10
|
+
"Client",
|
|
11
|
+
"AsyncClient",
|
|
12
|
+
"REDACTED",
|
|
13
|
+
"DEFAULT_REDACT_HEADERS",
|
|
14
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from httpx_debug._curl import to_curl
|
|
12
|
+
|
|
13
|
+
_TOOLS = (
|
|
14
|
+
["pbcopy"],
|
|
15
|
+
["wl-copy"],
|
|
16
|
+
["xclip", "-selection", "clipboard"],
|
|
17
|
+
["xsel", "--clipboard", "--input"],
|
|
18
|
+
["clip"],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def copy(request_or_text: Union[httpx.Request, str], *, redact: bool = True) -> bool:
|
|
23
|
+
"""Copy a request (as a curl command) or plain text to the clipboard.
|
|
24
|
+
|
|
25
|
+
Returns True on success; never raises.
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(request_or_text, str):
|
|
28
|
+
text = request_or_text
|
|
29
|
+
else:
|
|
30
|
+
text = to_curl(request_or_text, redact=redact)
|
|
31
|
+
return copy_text(text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def copy_text(text: str) -> bool:
|
|
35
|
+
return _native_copy(text) or _osc52_copy(text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _native_copy(text: str) -> bool:
|
|
39
|
+
for cmd in _TOOLS:
|
|
40
|
+
if not shutil.which(cmd[0]):
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
cmd,
|
|
45
|
+
input=text.encode("utf-8"),
|
|
46
|
+
stdout=subprocess.DEVNULL,
|
|
47
|
+
stderr=subprocess.DEVNULL,
|
|
48
|
+
timeout=5,
|
|
49
|
+
)
|
|
50
|
+
except Exception:
|
|
51
|
+
continue
|
|
52
|
+
if result.returncode == 0:
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _osc52_copy(text: str) -> bool:
|
|
58
|
+
# OSC 52 asks the terminal emulator to set the clipboard, so it reaches
|
|
59
|
+
# the *local* clipboard even from an SSH session
|
|
60
|
+
payload = base64.b64encode(text.encode("utf-8"))
|
|
61
|
+
try:
|
|
62
|
+
tty = _open_tty()
|
|
63
|
+
except Exception:
|
|
64
|
+
return False
|
|
65
|
+
try:
|
|
66
|
+
tty.write(b"\x1b]52;c;" + payload + b"\x07")
|
|
67
|
+
tty.flush()
|
|
68
|
+
return True
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
71
|
+
finally:
|
|
72
|
+
try:
|
|
73
|
+
tty.close()
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _open_tty():
|
|
79
|
+
if sys.platform == "win32":
|
|
80
|
+
raise OSError("OSC 52 fallback not supported on Windows")
|
|
81
|
+
return open("/dev/tty", "wb")
|
httpx_debug/_curl.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from typing import Iterable, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
REDACTED = "<redacted>"
|
|
9
|
+
|
|
10
|
+
DEFAULT_REDACT_HEADERS = frozenset(
|
|
11
|
+
{
|
|
12
|
+
"authorization",
|
|
13
|
+
"proxy-authorization",
|
|
14
|
+
"cookie",
|
|
15
|
+
"set-cookie",
|
|
16
|
+
"x-api-key",
|
|
17
|
+
"api-key",
|
|
18
|
+
"x-auth-token",
|
|
19
|
+
"x-amz-security-token",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# curl derives these itself; including them makes the command wrong when edited
|
|
24
|
+
_SKIP_HEADERS = frozenset({"host", "content-length"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_curl(
|
|
28
|
+
request: httpx.Request,
|
|
29
|
+
*,
|
|
30
|
+
redact: bool = True,
|
|
31
|
+
redact_headers: Optional[Iterable[str]] = None,
|
|
32
|
+
) -> str:
|
|
33
|
+
"""Render an httpx.Request as a single-line, shell-safe curl command.
|
|
34
|
+
|
|
35
|
+
Sensitive header values are replaced with ``<redacted>`` unless
|
|
36
|
+
``redact=False``. Streamed bodies are never consumed.
|
|
37
|
+
"""
|
|
38
|
+
sensitive = (
|
|
39
|
+
frozenset(h.lower() for h in redact_headers)
|
|
40
|
+
if redact_headers is not None
|
|
41
|
+
else DEFAULT_REDACT_HEADERS
|
|
42
|
+
)
|
|
43
|
+
tokens = ["curl"]
|
|
44
|
+
if request.method != "GET":
|
|
45
|
+
tokens += ["-X", request.method]
|
|
46
|
+
for name, value in request.headers.multi_items():
|
|
47
|
+
lower = name.lower()
|
|
48
|
+
if lower in _SKIP_HEADERS:
|
|
49
|
+
continue
|
|
50
|
+
if redact and lower in sensitive:
|
|
51
|
+
value = REDACTED
|
|
52
|
+
tokens += ["-H", f"{_display_name(name)}: {value}"]
|
|
53
|
+
body, comment = _body(request)
|
|
54
|
+
if body is not None:
|
|
55
|
+
tokens += ["-d", body]
|
|
56
|
+
tokens.append(str(request.url))
|
|
57
|
+
command = shlex.join(tokens)
|
|
58
|
+
if comment:
|
|
59
|
+
command += "\n" + comment
|
|
60
|
+
return command
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _display_name(name: str) -> str:
|
|
64
|
+
# httpx lowercases header names; Title-Case reads closer to what was sent
|
|
65
|
+
return "-".join(part.capitalize() for part in name.split("-")) if name.islower() else name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _body(request: httpx.Request) -> Tuple[Optional[str], Optional[str]]:
|
|
69
|
+
try:
|
|
70
|
+
raw = request.content
|
|
71
|
+
except httpx.RequestNotRead:
|
|
72
|
+
return None, "# request body was streamed and is not shown"
|
|
73
|
+
if not raw:
|
|
74
|
+
return None, None
|
|
75
|
+
try:
|
|
76
|
+
return raw.decode("utf-8"), None
|
|
77
|
+
except UnicodeDecodeError:
|
|
78
|
+
return None, f"# binary request body ({len(raw)} bytes) not shown"
|
httpx_debug/_hooks.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import IO, Iterable, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from httpx_debug import _clipboard
|
|
9
|
+
from httpx_debug._curl import to_curl
|
|
10
|
+
|
|
11
|
+
_MARKER = "_httpx_debug_hook"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Config:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
on: str = "error",
|
|
18
|
+
copy: bool = False,
|
|
19
|
+
redact: bool = True,
|
|
20
|
+
redact_headers: Optional[Iterable[str]] = None,
|
|
21
|
+
file: Optional[IO[str]] = None,
|
|
22
|
+
copy_redact: Optional[bool] = None,
|
|
23
|
+
):
|
|
24
|
+
if on not in ("error", "all"):
|
|
25
|
+
raise ValueError(f'on must be "error" or "all", got {on!r}')
|
|
26
|
+
self.on = on
|
|
27
|
+
self.copy = copy
|
|
28
|
+
self.redact = redact
|
|
29
|
+
self.redact_headers = redact_headers
|
|
30
|
+
self.file = file
|
|
31
|
+
# clipboard redaction follows `redact` unless explicitly overridden;
|
|
32
|
+
# copy_redact=False keeps printed output safe while the clipboard
|
|
33
|
+
# gets a replayable command with real credentials
|
|
34
|
+
self.copy_redact = redact if copy_redact is None else copy_redact
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _emit(request: httpx.Request, headline: str, config: _Config) -> None:
|
|
38
|
+
curl = to_curl(request, redact=config.redact, redact_headers=config.redact_headers)
|
|
39
|
+
out = config.file if config.file is not None else sys.stderr
|
|
40
|
+
print(f"# httpx-debug: {headline} ← {request.method} {request.url}", file=out)
|
|
41
|
+
print(curl, file=out)
|
|
42
|
+
if config.copy:
|
|
43
|
+
if config.copy_redact == config.redact:
|
|
44
|
+
clip = curl
|
|
45
|
+
else:
|
|
46
|
+
clip = to_curl(request, redact=config.copy_redact, redact_headers=config.redact_headers)
|
|
47
|
+
_clipboard.copy_text(clip)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _handle_response(response: httpx.Response, config: _Config) -> None:
|
|
51
|
+
if config.on == "error" and response.status_code < 400:
|
|
52
|
+
return
|
|
53
|
+
_emit(response.request, f"{response.status_code} {response.reason_phrase}", config)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def attach(
|
|
57
|
+
client,
|
|
58
|
+
*,
|
|
59
|
+
on: str = "error",
|
|
60
|
+
copy: bool = False,
|
|
61
|
+
redact: bool = True,
|
|
62
|
+
redact_headers: Optional[Iterable[str]] = None,
|
|
63
|
+
file: Optional[IO[str]] = None,
|
|
64
|
+
copy_redact: Optional[bool] = None,
|
|
65
|
+
):
|
|
66
|
+
"""Install a response event hook that prints (and optionally copies) the
|
|
67
|
+
curl command for failing requests. Works on httpx.Client and AsyncClient,
|
|
68
|
+
including clients handed to third-party SDKs.
|
|
69
|
+
|
|
70
|
+
Event hooks cannot observe transport exceptions (timeouts, DNS failures);
|
|
71
|
+
use httpx_debug.Client / httpx_debug.AsyncClient for those.
|
|
72
|
+
"""
|
|
73
|
+
config = _Config(
|
|
74
|
+
on=on,
|
|
75
|
+
copy=copy,
|
|
76
|
+
redact=redact,
|
|
77
|
+
redact_headers=redact_headers,
|
|
78
|
+
file=file,
|
|
79
|
+
copy_redact=copy_redact,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if isinstance(client, httpx.AsyncClient):
|
|
83
|
+
|
|
84
|
+
async def hook(response: httpx.Response) -> None:
|
|
85
|
+
_handle_response(response, config)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
|
|
89
|
+
def hook(response: httpx.Response) -> None: # type: ignore[misc]
|
|
90
|
+
_handle_response(response, config)
|
|
91
|
+
|
|
92
|
+
setattr(hook, _MARKER, True)
|
|
93
|
+
hooks = {name: list(fns) for name, fns in client.event_hooks.items()}
|
|
94
|
+
hooks.setdefault("response", []).append(hook)
|
|
95
|
+
client.event_hooks = hooks
|
|
96
|
+
return client
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def detach(client) -> None:
|
|
100
|
+
"""Remove hooks previously installed by attach()."""
|
|
101
|
+
client.event_hooks = {
|
|
102
|
+
name: [fn for fn in fns if not getattr(fn, _MARKER, False)]
|
|
103
|
+
for name, fns in client.event_hooks.items()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pop_debug_kwargs(kwargs: dict) -> _Config:
|
|
108
|
+
return _Config(
|
|
109
|
+
on=kwargs.pop("debug_on", "error"),
|
|
110
|
+
copy=kwargs.pop("debug_copy", False),
|
|
111
|
+
redact=kwargs.pop("debug_redact", True),
|
|
112
|
+
redact_headers=kwargs.pop("debug_redact_headers", None),
|
|
113
|
+
file=kwargs.pop("debug_file", None),
|
|
114
|
+
copy_redact=kwargs.pop("debug_copy_redact", None),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Client(httpx.Client):
|
|
119
|
+
"""Drop-in httpx.Client that also reports transport errors as curl."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, *args, **kwargs):
|
|
122
|
+
self._debug = _pop_debug_kwargs(kwargs)
|
|
123
|
+
super().__init__(*args, **kwargs)
|
|
124
|
+
|
|
125
|
+
def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
|
|
126
|
+
try:
|
|
127
|
+
response = super().send(request, **kwargs)
|
|
128
|
+
except httpx.RequestError as exc:
|
|
129
|
+
_emit(request, f"{type(exc).__name__}: {exc}", self._debug)
|
|
130
|
+
raise
|
|
131
|
+
_handle_response(response, self._debug)
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class AsyncClient(httpx.AsyncClient):
|
|
136
|
+
"""Drop-in httpx.AsyncClient that also reports transport errors as curl."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, *args, **kwargs):
|
|
139
|
+
self._debug = _pop_debug_kwargs(kwargs)
|
|
140
|
+
super().__init__(*args, **kwargs)
|
|
141
|
+
|
|
142
|
+
async def send(self, request: httpx.Request, **kwargs) -> httpx.Response:
|
|
143
|
+
try:
|
|
144
|
+
response = await super().send(request, **kwargs)
|
|
145
|
+
except httpx.RequestError as exc:
|
|
146
|
+
_emit(request, f"{type(exc).__name__}: {exc}", self._debug)
|
|
147
|
+
raise
|
|
148
|
+
_handle_response(response, self._debug)
|
|
149
|
+
return response
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: httpx-debug
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Failure-aware curl output for httpx: reproduce any (failing) request as a paste-able, secret-redacted curl command — printed or copied to your clipboard, even over SSH.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mayurrawte/httpx-debug
|
|
6
|
+
Author: Mayur Rawte
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: clipboard,curl,debug,debugging,devtools,httpx
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: httpx>=0.23
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# httpx-debug
|
|
20
|
+
|
|
21
|
+
**Reproduce any httpx request — especially the failing ones — as a paste-able curl command.**
|
|
22
|
+
|
|
23
|
+
Stop print-debugging your HTTP calls:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# before
|
|
27
|
+
print(self._headers())
|
|
28
|
+
print(params)
|
|
29
|
+
print(self.base_url)
|
|
30
|
+
print(path)
|
|
31
|
+
|
|
32
|
+
# after
|
|
33
|
+
import httpx_debug
|
|
34
|
+
httpx_debug.attach(client) # failing requests print a ready-to-paste curl command
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
When a request fails (4xx / 5xx / timeout / connection error), `httpx-debug` prints — and optionally copies to your clipboard — the exact `curl` command that reproduces it, with your secrets redacted:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
# httpx-debug: 401 Unauthorized ← POST https://api.example.com/v1/users
|
|
41
|
+
curl -X POST -H 'Authorization: <redacted>' -H 'Content-Type: application/json' -d '{"name": "mayur"}' https://api.example.com/v1/users
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Why not curlify?
|
|
45
|
+
|
|
46
|
+
[curlify](https://pypi.org/project/curlify/) and friends convert a request object to curl — and that's it. `httpx-debug` is the debugging layer around that:
|
|
47
|
+
|
|
48
|
+
- **Failure-aware** — attach once, hear about it only when something breaks. No call-site changes.
|
|
49
|
+
- **Secret redaction by default** — `Authorization`, `Cookie`, `X-API-Key`, … become `<redacted>` so you can paste the command into an issue or Slack without leaking credentials.
|
|
50
|
+
- **Clipboard, even over SSH** — uses `pbcopy`/`xclip`/`wl-copy`/`clip` locally and falls back to the OSC 52 terminal escape, which copies to your *local* clipboard from a remote shell.
|
|
51
|
+
- **Async-first** — works identically with `httpx.Client` and `httpx.AsyncClient`.
|
|
52
|
+
- **Sees exceptions too** — the drop-in `httpx_debug.Client` / `AsyncClient` also capture timeouts and connection errors, not just error status codes.
|
|
53
|
+
|
|
54
|
+
Works anywhere httpx does — including under the **OpenAI and Anthropic SDKs**, which accept a custom `http_client`. Ever wondered what your LLM call actually sends over the wire?
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import httpx, httpx_debug, openai
|
|
58
|
+
|
|
59
|
+
http_client = httpx.Client()
|
|
60
|
+
httpx_debug.attach(http_client, on="all")
|
|
61
|
+
client = openai.OpenAI(http_client=http_client)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install httpx-debug
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Zero dependencies beyond httpx itself.
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### Convert a request to curl
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import httpx_debug
|
|
78
|
+
|
|
79
|
+
curl = httpx_debug.to_curl(request) # secrets redacted
|
|
80
|
+
curl = httpx_debug.to_curl(request, redact=False) # raw
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Copy to clipboard
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
httpx_debug.copy(request) # returns True if a clipboard was reachable
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Attach to any client (yours or an SDK's)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client = httpx.AsyncClient()
|
|
93
|
+
httpx_debug.attach(client) # print curl for 4xx/5xx responses
|
|
94
|
+
httpx_debug.attach(client, on="all") # ...or for every request
|
|
95
|
+
httpx_debug.attach(client, copy=True) # also copy to clipboard
|
|
96
|
+
httpx_debug.detach(client) # remove
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`attach` uses httpx event hooks, so it can't observe transport exceptions (timeouts, DNS failures). For those, use the drop-in clients:
|
|
100
|
+
|
|
101
|
+
### Drop-in clients (also capture timeouts & connection errors)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
client = httpx_debug.AsyncClient(base_url="https://api.example.com")
|
|
105
|
+
# behaves exactly like httpx.AsyncClient, plus curl output on any failure
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Options
|
|
109
|
+
|
|
110
|
+
| Option | Default | |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `on` | `"error"` | `"error"` (status ≥ 400 + exceptions) or `"all"` |
|
|
113
|
+
| `copy` | `False` | also copy the curl command to the clipboard |
|
|
114
|
+
| `redact` | `True` | replace sensitive header values with `<redacted>` |
|
|
115
|
+
| `copy_redact` | follows `redact` | redaction for the *clipboard* copy specifically |
|
|
116
|
+
| `redact_headers` | built-in set | which headers count as sensitive (lowercase) |
|
|
117
|
+
| `file` | `sys.stderr` | where to print |
|
|
118
|
+
|
|
119
|
+
On the drop-in clients the same options are prefixed: `debug_on`, `debug_copy`, `debug_redact`, `debug_copy_redact`, `debug_redact_headers`, `debug_file`.
|
|
120
|
+
|
|
121
|
+
### Print safe, copy replayable
|
|
122
|
+
|
|
123
|
+
The printed command ends up in logs and screenshots — keep it redacted. The clipboard's job is *replaying* the request, which needs real credentials:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
httpx_debug.attach(client, copy=True, copy_redact=False)
|
|
127
|
+
# terminal: Authorization: <redacted> ← safe to paste in an issue
|
|
128
|
+
# clipboard: Authorization: Bearer sk-… ← actually runs
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Raw copying is always opt-in — remember clipboard managers keep history.
|
|
132
|
+
|
|
133
|
+
## Notes
|
|
134
|
+
|
|
135
|
+
- Streamed request bodies are never consumed by httpx-debug; the curl output notes `# request body was streamed and is not shown`.
|
|
136
|
+
- Non-UTF-8 bodies are omitted with a byte-count comment.
|
|
137
|
+
- Debug tooling must never break your app: clipboard and hook failures are swallowed.
|
|
138
|
+
|
|
139
|
+
## Roadmap
|
|
140
|
+
|
|
141
|
+
- pytest plugin: show the curl of the last failed request under the traceback
|
|
142
|
+
- response capture alongside the request
|
|
143
|
+
- `requests` support
|
|
144
|
+
- a Node.js sibling for server-side `fetch`
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
httpx_debug/__init__.py,sha256=jodJv2IpcOYTgOr0OCxnpWmTQ-gBlsVnbWn498LqWkg,328
|
|
2
|
+
httpx_debug/_clipboard.py,sha256=h9gQ3zu5mrMqmg0gRFovithx5YEfzlUKBqTqTOntJ2M,1951
|
|
3
|
+
httpx_debug/_curl.py,sha256=FvmD7Eam2SAhGV-iome03izJCopftzxy9XFqv0Onyfo,2228
|
|
4
|
+
httpx_debug/_hooks.py,sha256=Flgi5OfjJUf08Fe7EWGTpM_SZs9T927zla4WsATEek4,4925
|
|
5
|
+
httpx_debug-0.1.0.dist-info/METADATA,sha256=K2X4RIF9YbzNzIjEqBB1vw9dkrCUCZkboVprD3QOUzM,5483
|
|
6
|
+
httpx_debug-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
httpx_debug-0.1.0.dist-info/licenses/LICENSE,sha256=RFWd3lWPIDAthIAm9tYEbLpsSJVKWFCjXPhrblnH4Bk,1068
|
|
8
|
+
httpx_debug-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mayur Rawte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|