Files
MoFin/venv/lib/python3.12/site-packages/curl_cffi/cli/output.py
T
知微 fa45d8aa5f fix: 小果地址统一node122(兼容LAN+EasyTier)
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
  Privoxy对node122:18003返回500,直连正常
2026-06-30 02:56:35 +08:00

220 lines
6.6 KiB
Python

from __future__ import annotations
import argparse
import json
import re
import sys
from typing import Literal
from curl_cffi.const import CurlHttpVersion
from curl_cffi.requests import Response
from rich.console import Console
from rich.progress import Progress, BarColumn, DownloadColumn, TransferSpeedColumn
from rich.syntax import Syntax
from rich.text import Text
def _http_ver_label(response: Response) -> Literal["1.0", "1.1", "2", "3"]:
mapping = {
CurlHttpVersion.V1_0: "1.0",
CurlHttpVersion.V1_1: "1.1",
CurlHttpVersion.V2_0: "2",
CurlHttpVersion.V2TLS: "2",
CurlHttpVersion.V2_PRIOR_KNOWLEDGE: "2",
CurlHttpVersion.V3: "3",
CurlHttpVersion.V3ONLY: "3",
}
return mapping.get(response.http_version, "1.1") # type: ignore
def determine_print_spec(args: argparse.Namespace) -> str:
if args.print_spec:
return args.print_spec
if args.verbose:
return "HhBb"
if args.headers_only:
return "h"
if args.body_only:
return "b"
if args.download:
return "h"
if sys.stdout.isatty(): # interactive terminal
return "hb"
return "b"
def _print_headers(console: Console, lines: list[str], use_color: bool) -> None:
"""Print HTTP headers, with rich colors when possible."""
if not use_color:
for line in lines:
print(line)
return
for line in lines:
if ": " in line:
key, _, value = line.partition(": ")
text = Text()
text.append(key, style="bold cyan")
text.append(": ", style="dim")
text.append(value)
console.print(text)
else:
console.print(Text(line, style="bold green"))
def _print_body(console: Console, response: Response, use_color: bool) -> None:
"""Print response body, with syntax highlight when possible."""
content_type = response.headers.get("content-type", "")
if "json" in content_type:
try:
formatted = json.dumps(response.json(), indent=2, ensure_ascii=False)
if use_color:
console.print(
Syntax(
formatted,
"json",
theme="ansi_dark",
word_wrap=True,
background_color="default",
)
)
else:
print(formatted)
return
except (json.JSONDecodeError, ValueError):
pass
if content_type.startswith("image/"):
print(
f"Binary image data ({content_type}, {len(response.content)} bytes)",
file=sys.stderr,
)
return
if not use_color:
print(response.text)
return
if "html" in content_type:
console.print(
Syntax(
response.text,
"html",
theme="ansi_dark",
word_wrap=True,
background_color="default",
)
)
elif "xml" in content_type:
console.print(
Syntax(
response.text,
"xml",
theme="ansi_dark",
word_wrap=True,
background_color="default",
)
)
else:
console.print(response.text, highlight=False, markup=False)
def _print_status(console: Console, response: Response, use_color: bool) -> None:
"""Print the HTTP status line."""
ver = _http_ver_label(response)
status_line = f"HTTP/{ver} {response.status_code} {response.reason}"
if not use_color:
print(status_line)
return
if response.status_code < 300:
style = "bold green"
elif response.status_code < 400:
style = "bold yellow"
else:
style = "bold red"
console.print(Text(status_line, style=style))
def print_output(
response: Response,
method: str,
url: str,
request_headers: dict[str, str] | None,
request_body: str | None,
print_spec: str,
) -> None:
use_color = sys.stdout.isatty() # interactive terminal
console = Console(force_terminal=use_color, no_color=not use_color)
# print request headers
if "H" in print_spec:
lines = [f"{method} {url}"]
for k, v in (request_headers or {}).items():
lines.append(f"{k}: {v}")
_print_headers(console, lines, use_color)
print()
# print request body
if "B" in print_spec and request_body:
if use_color:
console.print(
Syntax(
request_body,
"json",
theme="ansi_dark",
word_wrap=True,
background_color="default",
)
)
else:
print(request_body)
print()
# print response headers
if "h" in print_spec:
_print_status(console, response, use_color)
header_lines = [f"{k}: {v}" for k, v in response.headers.items()]
_print_headers(console, header_lines, use_color)
print()
# print response body
if "b" in print_spec:
_print_body(console, response, use_color)
def _sanitize_filename(name: str) -> str:
"""Remove or replace characters unsafe for filenames."""
# Strip directory traversal and null bytes
name = name.replace("\x00", "").split("/")[-1].split("\\")[-1]
# Keep only safe characters
name = re.sub(r"[^\w.\-]", "_", name)
return name or "download"
def handle_download(
response: Response, url: str, output_path: str | None = None
) -> None:
if output_path is None:
cd = response.headers.get("content-disposition", "")
if "filename=" in cd:
output_path = cd.split("filename=")[1].strip('"').strip("'")
else:
output_path = url.rstrip("/").split("/")[-1] or "download"
output_path = _sanitize_filename(output_path)
content = response.content
total = len(content)
console = Console(stderr=True)
with Progress(
"[progress.description]{task.description}",
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
console=console,
) as progress:
task = progress.add_task(output_path, total=total)
with open(output_path, "wb") as f:
chunk_size = 64 * 1024
for offset in range(0, total, chunk_size):
chunk = content[offset : offset + chunk_size]
f.write(chunk)
progress.advance(task, len(chunk))
print(f"Downloaded to {output_path}", file=sys.stderr)