diff options
Diffstat (limited to 'venv/lib/python3.11/site-packages/httpx/_main.py')
| -rw-r--r-- | venv/lib/python3.11/site-packages/httpx/_main.py | 509 | 
1 files changed, 509 insertions, 0 deletions
| diff --git a/venv/lib/python3.11/site-packages/httpx/_main.py b/venv/lib/python3.11/site-packages/httpx/_main.py new file mode 100644 index 0000000..72657f8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/httpx/_main.py @@ -0,0 +1,509 @@ +from __future__ import annotations + +import functools +import json +import sys +import typing + +import click +import httpcore +import pygments.lexers +import pygments.util +import rich.console +import rich.markup +import rich.progress +import rich.syntax +import rich.table + +from ._client import Client +from ._exceptions import RequestError +from ._models import Response +from ._status_codes import codes + + +def print_help() -> None: +    console = rich.console.Console() + +    console.print("[bold]HTTPX :butterfly:", justify="center") +    console.print() +    console.print("A next generation HTTP client.", justify="center") +    console.print() +    console.print( +        "Usage: [bold]httpx[/bold] [cyan]<URL> [OPTIONS][/cyan] ", justify="left" +    ) +    console.print() + +    table = rich.table.Table.grid(padding=1, pad_edge=True) +    table.add_column("Parameter", no_wrap=True, justify="left", style="bold") +    table.add_column("Description") +    table.add_row( +        "-m, --method [cyan]METHOD", +        "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n" +        "[Default: GET, or POST if a request body is included]", +    ) +    table.add_row( +        "-p, --params [cyan]<NAME VALUE> ...", +        "Query parameters to include in the request URL.", +    ) +    table.add_row( +        "-c, --content [cyan]TEXT", "Byte content to include in the request body." +    ) +    table.add_row( +        "-d, --data [cyan]<NAME VALUE> ...", "Form data to include in the request body." +    ) +    table.add_row( +        "-f, --files [cyan]<NAME FILENAME> ...", +        "Form files to include in the request body.", +    ) +    table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.") +    table.add_row( +        "-h, --headers [cyan]<NAME VALUE> ...", +        "Include additional HTTP headers in the request.", +    ) +    table.add_row( +        "--cookies [cyan]<NAME VALUE> ...", "Cookies to include in the request." +    ) +    table.add_row( +        "--auth [cyan]<USER PASS>", +        "Username and password to include in the request. Specify '-' for the password" +        " to use a password prompt. Note that using --verbose/-v will expose" +        " the Authorization header, including the password encoding" +        " in a trivially reversible format.", +    ) + +    table.add_row( +        "--proxy [cyan]URL", +        "Send the request via a proxy. Should be the URL giving the proxy address.", +    ) + +    table.add_row( +        "--timeout [cyan]FLOAT", +        "Timeout value to use for network operations, such as establishing the" +        " connection, reading some data, etc... [Default: 5.0]", +    ) + +    table.add_row("--follow-redirects", "Automatically follow redirects.") +    table.add_row("--no-verify", "Disable SSL verification.") +    table.add_row( +        "--http2", "Send the request using HTTP/2, if the remote server supports it." +    ) + +    table.add_row( +        "--download [cyan]FILE", +        "Save the response content as a file, rather than displaying it.", +    ) + +    table.add_row("-v, --verbose", "Verbose output. Show request as well as response.") +    table.add_row("--help", "Show this message and exit.") +    console.print(table) + + +def get_lexer_for_response(response: Response) -> str: +    content_type = response.headers.get("Content-Type") +    if content_type is not None: +        mime_type, _, _ = content_type.partition(";") +        try: +            return typing.cast( +                str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name +            ) +        except pygments.util.ClassNotFound:  # pragma: no cover +            pass +    return ""  # pragma: no cover + + +def format_request_headers(request: httpcore.Request, http2: bool = False) -> str: +    version = "HTTP/2" if http2 else "HTTP/1.1" +    headers = [ +        (name.lower() if http2 else name, value) for name, value in request.headers +    ] +    method = request.method.decode("ascii") +    target = request.url.target.decode("ascii") +    lines = [f"{method} {target} {version}"] + [ +        f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers +    ] +    return "\n".join(lines) + + +def format_response_headers( +    http_version: bytes, +    status: int, +    reason_phrase: bytes | None, +    headers: list[tuple[bytes, bytes]], +) -> str: +    version = http_version.decode("ascii") +    reason = ( +        codes.get_reason_phrase(status) +        if reason_phrase is None +        else reason_phrase.decode("ascii") +    ) +    lines = [f"{version} {status} {reason}"] + [ +        f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers +    ] +    return "\n".join(lines) + + +def print_request_headers(request: httpcore.Request, http2: bool = False) -> None: +    console = rich.console.Console() +    http_text = format_request_headers(request, http2=http2) +    syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) +    console.print(syntax) +    syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) +    console.print(syntax) + + +def print_response_headers( +    http_version: bytes, +    status: int, +    reason_phrase: bytes | None, +    headers: list[tuple[bytes, bytes]], +) -> None: +    console = rich.console.Console() +    http_text = format_response_headers(http_version, status, reason_phrase, headers) +    syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) +    console.print(syntax) +    syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) +    console.print(syntax) + + +def print_response(response: Response) -> None: +    console = rich.console.Console() +    lexer_name = get_lexer_for_response(response) +    if lexer_name: +        if lexer_name.lower() == "json": +            try: +                data = response.json() +                text = json.dumps(data, indent=4) +            except ValueError:  # pragma: no cover +                text = response.text +        else: +            text = response.text + +        syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True) +        console.print(syntax) +    else: +        console.print(f"<{len(response.content)} bytes of binary data>") + + +_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...] +_PCTRTTT = typing.Tuple[_PCTRTT, ...] +_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]] + + +def format_certificate(cert: _PeerCertRetDictType) -> str:  # pragma: no cover +    lines = [] +    for key, value in cert.items(): +        if isinstance(value, (list, tuple)): +            lines.append(f"*   {key}:") +            for item in value: +                if key in ("subject", "issuer"): +                    for sub_item in item: +                        lines.append(f"*     {sub_item[0]}: {sub_item[1]!r}") +                elif isinstance(item, tuple) and len(item) == 2: +                    lines.append(f"*     {item[0]}: {item[1]!r}") +                else: +                    lines.append(f"*     {item!r}") +        else: +            lines.append(f"*   {key}: {value!r}") +    return "\n".join(lines) + + +def trace( +    name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False +) -> None: +    console = rich.console.Console() +    if name == "connection.connect_tcp.started" and verbose: +        host = info["host"] +        console.print(f"* Connecting to {host!r}") +    elif name == "connection.connect_tcp.complete" and verbose: +        stream = info["return_value"] +        server_addr = stream.get_extra_info("server_addr") +        console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}") +    elif name == "connection.start_tls.complete" and verbose:  # pragma: no cover +        stream = info["return_value"] +        ssl_object = stream.get_extra_info("ssl_object") +        version = ssl_object.version() +        cipher = ssl_object.cipher() +        server_cert = ssl_object.getpeercert() +        alpn = ssl_object.selected_alpn_protocol() +        console.print(f"* SSL established using {version!r} / {cipher[0]!r}") +        console.print(f"* Selected ALPN protocol: {alpn!r}") +        if server_cert: +            console.print("* Server certificate:") +            console.print(format_certificate(server_cert)) +    elif name == "http11.send_request_headers.started" and verbose: +        request = info["request"] +        print_request_headers(request, http2=False) +    elif name == "http2.send_request_headers.started" and verbose:  # pragma: no cover +        request = info["request"] +        print_request_headers(request, http2=True) +    elif name == "http11.receive_response_headers.complete": +        http_version, status, reason_phrase, headers = info["return_value"] +        print_response_headers(http_version, status, reason_phrase, headers) +    elif name == "http2.receive_response_headers.complete":  # pragma: no cover +        status, headers = info["return_value"] +        http_version = b"HTTP/2" +        reason_phrase = None +        print_response_headers(http_version, status, reason_phrase, headers) + + +def download_response(response: Response, download: typing.BinaryIO) -> None: +    console = rich.console.Console() +    console.print() +    content_length = response.headers.get("Content-Length") +    with rich.progress.Progress( +        "[progress.description]{task.description}", +        "[progress.percentage]{task.percentage:>3.0f}%", +        rich.progress.BarColumn(bar_width=None), +        rich.progress.DownloadColumn(), +        rich.progress.TransferSpeedColumn(), +    ) as progress: +        description = f"Downloading [bold]{rich.markup.escape(download.name)}" +        download_task = progress.add_task( +            description, +            total=int(content_length or 0), +            start=content_length is not None, +        ) +        for chunk in response.iter_bytes(): +            download.write(chunk) +            progress.update(download_task, completed=response.num_bytes_downloaded) + + +def validate_json( +    ctx: click.Context, +    param: click.Option | click.Parameter, +    value: typing.Any, +) -> typing.Any: +    if value is None: +        return None + +    try: +        return json.loads(value) +    except json.JSONDecodeError:  # pragma: no cover +        raise click.BadParameter("Not valid JSON") + + +def validate_auth( +    ctx: click.Context, +    param: click.Option | click.Parameter, +    value: typing.Any, +) -> typing.Any: +    if value == (None, None): +        return None + +    username, password = value +    if password == "-":  # pragma: no cover +        password = click.prompt("Password", hide_input=True) +    return (username, password) + + +def handle_help( +    ctx: click.Context, +    param: click.Option | click.Parameter, +    value: typing.Any, +) -> None: +    if not value or ctx.resilient_parsing: +        return + +    print_help() +    ctx.exit() + + +@click.command(add_help_option=False) +@click.argument("url", type=str) +@click.option( +    "--method", +    "-m", +    "method", +    type=str, +    help=( +        "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. " +        "[Default: GET, or POST if a request body is included]" +    ), +) +@click.option( +    "--params", +    "-p", +    "params", +    type=(str, str), +    multiple=True, +    help="Query parameters to include in the request URL.", +) +@click.option( +    "--content", +    "-c", +    "content", +    type=str, +    help="Byte content to include in the request body.", +) +@click.option( +    "--data", +    "-d", +    "data", +    type=(str, str), +    multiple=True, +    help="Form data to include in the request body.", +) +@click.option( +    "--files", +    "-f", +    "files", +    type=(str, click.File(mode="rb")), +    multiple=True, +    help="Form files to include in the request body.", +) +@click.option( +    "--json", +    "-j", +    "json", +    type=str, +    callback=validate_json, +    help="JSON data to include in the request body.", +) +@click.option( +    "--headers", +    "-h", +    "headers", +    type=(str, str), +    multiple=True, +    help="Include additional HTTP headers in the request.", +) +@click.option( +    "--cookies", +    "cookies", +    type=(str, str), +    multiple=True, +    help="Cookies to include in the request.", +) +@click.option( +    "--auth", +    "auth", +    type=(str, str), +    default=(None, None), +    callback=validate_auth, +    help=( +        "Username and password to include in the request. " +        "Specify '-' for the password to use a password prompt. " +        "Note that using --verbose/-v will expose the Authorization header, " +        "including the password encoding in a trivially reversible format." +    ), +) +@click.option( +    "--proxy", +    "proxy", +    type=str, +    default=None, +    help="Send the request via a proxy. Should be the URL giving the proxy address.", +) +@click.option( +    "--timeout", +    "timeout", +    type=float, +    default=5.0, +    help=( +        "Timeout value to use for network operations, such as establishing the " +        "connection, reading some data, etc... [Default: 5.0]" +    ), +) +@click.option( +    "--follow-redirects", +    "follow_redirects", +    is_flag=True, +    default=False, +    help="Automatically follow redirects.", +) +@click.option( +    "--no-verify", +    "verify", +    is_flag=True, +    default=True, +    help="Disable SSL verification.", +) +@click.option( +    "--http2", +    "http2", +    type=bool, +    is_flag=True, +    default=False, +    help="Send the request using HTTP/2, if the remote server supports it.", +) +@click.option( +    "--download", +    type=click.File("wb"), +    help="Save the response content as a file, rather than displaying it.", +) +@click.option( +    "--verbose", +    "-v", +    type=bool, +    is_flag=True, +    default=False, +    help="Verbose. Show request as well as response.", +) +@click.option( +    "--help", +    is_flag=True, +    is_eager=True, +    expose_value=False, +    callback=handle_help, +    help="Show this message and exit.", +) +def main( +    url: str, +    method: str, +    params: list[tuple[str, str]], +    content: str, +    data: list[tuple[str, str]], +    files: list[tuple[str, click.File]], +    json: str, +    headers: list[tuple[str, str]], +    cookies: list[tuple[str, str]], +    auth: tuple[str, str] | None, +    proxy: str, +    timeout: float, +    follow_redirects: bool, +    verify: bool, +    http2: bool, +    download: typing.BinaryIO | None, +    verbose: bool, +) -> None: +    """ +    An HTTP command line client. +    Sends a request and displays the response. +    """ +    if not method: +        method = "POST" if content or data or files or json else "GET" + +    try: +        with Client( +            proxy=proxy, +            timeout=timeout, +            verify=verify, +            http2=http2, +        ) as client: +            with client.stream( +                method, +                url, +                params=list(params), +                content=content, +                data=dict(data), +                files=files,  # type: ignore +                json=json, +                headers=headers, +                cookies=dict(cookies), +                auth=auth, +                follow_redirects=follow_redirects, +                extensions={"trace": functools.partial(trace, verbose=verbose)}, +            ) as response: +                if download is not None: +                    download_response(response, download) +                else: +                    response.read() +                    if response.content: +                        print_response(response) + +    except RequestError as exc: +        console = rich.console.Console() +        console.print(f"[red]{type(exc).__name__}[/red]: {exc}") +        sys.exit(1) + +    sys.exit(0 if response.is_success else 1) | 
