Skip to content

Commit

Permalink
⚗️ Try compatible fork Niquests to supercharge HTTPie
Browse files Browse the repository at this point in the history
  • Loading branch information
Ousret committed Oct 3, 2023
1 parent e52a60e commit 46f030c
Show file tree
Hide file tree
Showing 35 changed files with 218 additions and 106 deletions.
2 changes: 1 addition & 1 deletion httpie/adapters.py
@@ -1,5 +1,5 @@
from httpie.cli.dicts import HTTPHeadersDict
from requests.adapters import HTTPAdapter
from niquests.adapters import HTTPAdapter


class HTTPieHTTPAdapter(HTTPAdapter):
Expand Down
2 changes: 1 addition & 1 deletion httpie/cli/argparser.py
Expand Up @@ -7,7 +7,7 @@
from textwrap import dedent
from urllib.parse import urlsplit

from requests.utils import get_netrc_auth
from niquests.utils import get_netrc_auth

from .argtypes import (
AuthCredentials, SSLCredentials, KeyValueArgType,
Expand Down
4 changes: 2 additions & 2 deletions httpie/cli/definition.py
Expand Up @@ -193,7 +193,7 @@
content_types.add_argument(
'--boundary',
short_help=(
'Specify a custom boundary string for multipart/form-data requests. '
'Specify a custom boundary string for multipart/form-data niquests. '
'Only has effect only together with --form.'
)
)
Expand Down Expand Up @@ -594,7 +594,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
help="""
Create, or reuse and update a session. Within a session, custom headers,
auth credential, as well as any cookies sent by the server persist between
requests.
niquests.
Session files are stored in:
Expand Down
2 changes: 1 addition & 1 deletion httpie/cli/requestitems.py
Expand Up @@ -30,7 +30,7 @@ def __init__(self, request_type: Optional[RequestType] = None):
self.data = RequestJSONDataDict() if self.is_json else RequestDataDict()
self.files = RequestFilesDict()
self.params = RequestQueryParamsDict()
# To preserve the order of fields in file upload multipart requests.
# To preserve the order of fields in file upload multipart niquests.
self.multipart_data = MultipartRequestDataDict()

@classmethod
Expand Down
30 changes: 18 additions & 12 deletions httpie/client.py
Expand Up @@ -7,7 +7,7 @@
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse

import requests
import niquests
# noinspection PyPackageRequirements
import urllib3
from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS
Expand Down Expand Up @@ -44,6 +44,7 @@ def collect_messages(
env: Environment,
args: argparse.Namespace,
request_body_read_callback: Callable[[bytes], None] = None,
prepared_request_readiness: Callable[[niquests.PreparedRequest], None] = None,
) -> Iterable[RequestsMessage]:
httpie_session = None
httpie_session_headers = None
Expand Down Expand Up @@ -88,7 +89,12 @@ def collect_messages(
# TODO: reflect the split between request and send kwargs.
dump_request(request_kwargs)

request = requests.Request(**request_kwargs)
hooks = None

if prepared_request_readiness:
hooks = {"pre_send": [prepared_request_readiness]}

request = niquests.Request(**request_kwargs, hooks=hooks)
prepared_request = requests_session.prepare_request(request)
transform_headers(request, prepared_request)
if args.path_as_is:
Expand Down Expand Up @@ -124,7 +130,7 @@ def collect_messages(
response_count += 1
if response.next:
if args.max_redirects and response_count == args.max_redirects:
raise requests.TooManyRedirects
raise niquests.TooManyRedirects
if args.follow:
prepared_request = response.next
if args.all:
Expand Down Expand Up @@ -157,8 +163,8 @@ def build_requests_session(
verify: bool,
ssl_version: str = None,
ciphers: str = None,
) -> requests.Session:
requests_session = requests.Session()
) -> niquests.Session:
requests_session = niquests.Session()

# Install our adapter.
http_adapter = HTTPieHTTPAdapter()
Expand Down Expand Up @@ -186,7 +192,7 @@ def build_requests_session(

def dump_request(kwargs: dict):
sys.stderr.write(
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
f'\n>>> niquests.request(**{repr_dict(kwargs)})\n\n')


def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
Expand All @@ -210,13 +216,13 @@ def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:


def transform_headers(
request: requests.Request,
prepared_request: requests.PreparedRequest
request: niquests.Request,
prepared_request: niquests.PreparedRequest
) -> None:
"""Apply various transformations on top of the `prepared_requests`'s
headers to change the request prepreation behavior."""

# Remove 'Content-Length' when it is misplaced by requests.
# Remove 'Content-Length' when it is misplaced by niquests.
if (
prepared_request.method in IGNORE_CONTENT_LENGTH_METHODS
and prepared_request.headers.get('Content-Length') == '0'
Expand All @@ -232,7 +238,7 @@ def transform_headers(

def apply_missing_repeated_headers(
original_headers: HTTPHeadersDict,
prepared_request: requests.PreparedRequest
prepared_request: niquests.PreparedRequest
) -> None:
"""Update the given `prepared_request`'s headers with the original
ones. This allows the requests to be prepared as usual, and then later
Expand Down Expand Up @@ -291,7 +297,7 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
cert = args.cert
if args.cert_key:
# Having a client certificate key passphrase is not supported
# by requests. So we are using our own transportation structure
# by niquests. So we are using our own transportation structure
# which is compatible with their format (a tuple of minimum two
# items).
#
Expand Down Expand Up @@ -329,7 +335,7 @@ def make_request_kwargs(
request_body_read_callback=lambda chunk: chunk
) -> dict:
"""
Translate our `args` into `requests.Request` keyword arguments.
Translate our `args` into `niquests.Request` keyword arguments.
"""
files = args.files
Expand Down
57 changes: 49 additions & 8 deletions httpie/core.py
Expand Up @@ -5,9 +5,9 @@
import socket
from typing import List, Optional, Union, Callable

import requests
import niquests
from pygments import __version__ as pygments_version
from requests import __version__ as requests_version
from niquests import __version__ as requests_version

from . import __version__ as httpie_version
from .cli.constants import OUT_REQ_BODY
Expand Down Expand Up @@ -112,16 +112,16 @@ def handle_generic_error(e, annotation=None):
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except requests.Timeout:
except niquests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
except requests.TooManyRedirects:
except niquests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
env.log_error(
f'Too many redirects'
f' (--max-redirects={parsed_args.max_redirects}).'
)
except requests.exceptions.ConnectionError as exc:
except niquests.exceptions.ConnectionError as exc:
annotation = None
original_exc = unwrap_context(exc)
if isinstance(original_exc, socket.gaierror):
Expand Down Expand Up @@ -175,8 +175,8 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
exit_status = ExitStatus.SUCCESS
downloader = None
initial_request: Optional[requests.PreparedRequest] = None
final_response: Optional[requests.Response] = None
initial_request: Optional[niquests.PreparedRequest] = None
final_response: Optional[niquests.Response] = None
processing_options = ProcessingOptions.from_raw_args(args)

def separate():
Expand Down Expand Up @@ -204,8 +204,46 @@ def request_body_read_callback(chunk: bytes):
args.follow = True # --download implies --follow.
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
downloader.pre_request(args.headers)

def prepared_request_readiness(pr):
nonlocal output_options, do_write_body, processing_options

if initial_request == pr:
if args.debug and pr.conn_info and pr.conn_info.destination_address:
print(f"* Connected to {pr.conn_info.destination_address[0]} port {pr.conn_info.destination_address[1]}")
separate()

if args.debug and pr.conn_info:
if pr.conn_info.cipher:
print(f"* TLS connection using {pr.conn_info.tls_version.name.replace('_', '.')} / {pr.conn_info.cipher}")
separate()

if pr.conn_info.certificate_dict:
print("* Server certificate:")

if "subject" in pr.conn_info.certificate_dict:
print(f"* subject: {pr.conn_info.certificate_dict['subject']}")

print(f"* start date: {pr.conn_info.certificate_dict['notBefore']}")
print(f"* expire date: {pr.conn_info.certificate_dict['notAfter']}")

if "subjectAltName" in pr.conn_info.certificate_dict:
print(f"* subjectAltName: {pr.conn_info.certificate_dict['subjectAltName']}")

print(f"* issuer: {pr.conn_info.certificate_dict['issuer']}")
separate()

write_message(
requests_message=pr,
env=env,
output_options=output_options._replace(
body=do_write_body
),
processing_options=processing_options
)

messages = collect_messages(env, args=args,
request_body_read_callback=request_body_read_callback)
request_body_read_callback=request_body_read_callback, prepared_request_readiness=prepared_request_readiness)
force_separator = False
prev_with_body = False

Expand All @@ -225,6 +263,9 @@ def request_body_read_callback(chunk: bytes):
is_streamed_upload = not isinstance(message.body, (str, bytes))
do_write_body = not is_streamed_upload
force_separator = is_streamed_upload and env.stdout_isatty
if message.conn_info is None and not args.offline:
prev_with_body = output_options.body
continue
else:
final_response = message
if args.check_status or downloader:
Expand Down
6 changes: 3 additions & 3 deletions httpie/downloads.py
Expand Up @@ -10,7 +10,7 @@
from typing import IO, Optional, Tuple
from urllib.parse import urlsplit

import requests
import niquests

from .models import HTTPResponse, OutputOptions
from .output.streams import RawStream
Expand Down Expand Up @@ -202,7 +202,7 @@ def pre_request(self, request_headers: dict):
def start(
self,
initial_url: str,
final_response: requests.Response
final_response: niquests.Response
) -> Tuple[RawStream, IO]:
"""
Initiate and return a stream for `response` body with progress
Expand Down Expand Up @@ -288,7 +288,7 @@ def chunk_downloaded(self, chunk: bytes):
@staticmethod
def _get_output_file_from_response(
initial_url: str,
final_response: requests.Response,
final_response: niquests.Response,
) -> IO:
# Output file not specified. Pick a name that doesn't exist yet.
filename = None
Expand Down
4 changes: 2 additions & 2 deletions httpie/internal/update_warnings.py
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Any, Optional, Callable

import requests
import niquests

import httpie
from httpie.context import Environment, LogLevel
Expand Down Expand Up @@ -41,7 +41,7 @@ def _fetch_updates(env: Environment) -> str:
file = env.config.version_info_file
data = _read_data_error_free(file)

response = requests.get(PACKAGE_INDEX_LINK, verify=False)
response = niquests.get(PACKAGE_INDEX_LINK, verify=False)
response.raise_for_status()

data.setdefault('last_warned_date', None)
Expand Down
2 changes: 1 addition & 1 deletion httpie/manager/cli.py
Expand Up @@ -133,7 +133,7 @@ def generate_subparsers(root, parent_parser, definitions, spec):
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
Be aware that you might be looking for http/https commands for sending
HTTP requests. This command is only available for managing the HTTTPie
HTTP niquests. This command is only available for managing the HTTTPie
plugins and the configuration around it.
'''
),
Expand Down
21 changes: 12 additions & 9 deletions httpie/models.py
@@ -1,6 +1,6 @@
from time import monotonic

import requests
import niquests
from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS

from enum import Enum, auto
Expand Down Expand Up @@ -59,7 +59,7 @@ def content_type(self) -> str:


class HTTPResponse(HTTPMessage):
"""A :class:`requests.models.Response` wrapper."""
"""A :class:`niquests.models.Response` wrapper."""

def iter_body(self, chunk_size=1):
return self._orig.iter_content(chunk_size=chunk_size)
Expand Down Expand Up @@ -112,7 +112,8 @@ def version(self) -> str:
9: '0.9',
10: '1.0',
11: '1.1',
20: '2.0',
20: '2',
30: '3',
}
fallback = 11
version = None
Expand All @@ -128,7 +129,7 @@ def version(self) -> str:


class HTTPRequest(HTTPMessage):
"""A :class:`requests.models.Request` wrapper."""
"""A :class:`niquests.models.Request` wrapper."""

def iter_body(self, chunk_size):
yield self.body
Expand All @@ -140,10 +141,11 @@ def iter_lines(self, chunk_size):
def headers(self):
url = urlsplit(self._orig.url)

request_line = '{method} {path}{query} HTTP/1.1'.format(
request_line = '{method} {path}{query} {http_version}'.format(
method=self._orig.method,
path=url.path or '/',
query=f'?{url.query}' if url.query else ''
query=f'?{url.query}' if url.query else '',
http_version=self._orig.conn_info.http_version.value.replace(".0", "") if self._orig.conn_info and self._orig.conn_info.http_version else "HTTP/1.1"
)

headers = self._orig.headers.copy()
Expand All @@ -158,6 +160,7 @@ def headers(self):

headers.insert(0, request_line)
headers = '\r\n'.join(headers).strip()

return headers

@property
Expand All @@ -169,7 +172,7 @@ def body(self):
return body or b''


RequestsMessage = Union[requests.PreparedRequest, requests.Response]
RequestsMessage = Union[niquests.PreparedRequest, niquests.Response]


class RequestsMessageKind(Enum):
Expand All @@ -178,9 +181,9 @@ class RequestsMessageKind(Enum):


def infer_requests_message_kind(message: RequestsMessage) -> RequestsMessageKind:
if isinstance(message, requests.PreparedRequest):
if isinstance(message, niquests.PreparedRequest):
return RequestsMessageKind.REQUEST
elif isinstance(message, requests.Response):
elif isinstance(message, niquests.Response):
return RequestsMessageKind.RESPONSE
else:
raise TypeError(f"Unexpected message type: {type(message).__name__}")
Expand Down
4 changes: 2 additions & 2 deletions httpie/output/writer.py
@@ -1,5 +1,5 @@
import errno
import requests
import niquests
from typing import Any, Dict, IO, Optional, TextIO, Tuple, Type, Union

from ..cli.dicts import HTTPHeadersDict
Expand Down Expand Up @@ -105,7 +105,7 @@ def write_raw_data(
headers: Optional[HTTPHeadersDict] = None,
stream_kwargs: Optional[Dict[str, Any]] = None
):
msg = requests.PreparedRequest()
msg = niquests.PreparedRequest()
msg.is_body_upload_chunk = True
msg.body = data
msg.headers = headers or HTTPHeadersDict()
Expand Down

0 comments on commit 46f030c

Please sign in to comment.