Skip to content

Commit 741017e

Browse files
author
Ahmed TAHRI
committed
⚗️ Try compatible fork Niquests to supercharge HTTPie
1 parent e52a60e commit 741017e

37 files changed

+252
-195
lines changed

.github/workflows/tests.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ jobs:
2525
fail-fast: false
2626
matrix:
2727
os: [ubuntu-latest, macos-latest, windows-latest]
28-
python-version: [3.7, 3.8, 3.9, "3.10"]
29-
pyopenssl: [0, 1]
28+
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
3029
runs-on: ${{ matrix.os }}
3130
steps:
3231
- uses: actions/checkout@v3
@@ -39,12 +38,8 @@ jobs:
3938
python -m pip install --upgrade pip wheel
4039
python -m pip install --upgrade '.[dev]'
4140
python -m pytest --verbose ./httpie ./tests
42-
env:
43-
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
4441
- name: Linux & Mac setup
4542
if: matrix.os != 'windows-latest'
4643
run: |
4744
make install
4845
make test
49-
env:
50-
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}

httpie/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
"""
55

6-
__version__ = '3.2.2'
7-
__date__ = '2022-05-06'
6+
__version__ = '4.0.0'
7+
__date__ = '2023-10-11'
88
__author__ = 'Jakub Roztocil'
99
__licence__ = 'BSD'

httpie/adapters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from httpie.cli.dicts import HTTPHeadersDict
2-
from requests.adapters import HTTPAdapter
2+
from niquests.adapters import HTTPAdapter
33

44

55
class HTTPieHTTPAdapter(HTTPAdapter):

httpie/cli/argparser.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from textwrap import dedent
88
from urllib.parse import urlsplit
99

10-
from requests.utils import get_netrc_auth
10+
from niquests.utils import get_netrc_auth
1111

1212
from .argtypes import (
1313
AuthCredentials, SSLCredentials, KeyValueArgType,

httpie/cli/definition.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@
193193
content_types.add_argument(
194194
'--boundary',
195195
short_help=(
196-
'Specify a custom boundary string for multipart/form-data requests. '
196+
'Specify a custom boundary string for multipart/form-data niquests. '
197197
'Only has effect only together with --form.'
198198
)
199199
)
@@ -594,7 +594,7 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
594594
help="""
595595
Create, or reuse and update a session. Within a session, custom headers,
596596
auth credential, as well as any cookies sent by the server persist between
597-
requests.
597+
niquests.
598598
599599
Session files are stored in:
600600

httpie/cli/requestitems.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, request_type: Optional[RequestType] = None):
3030
self.data = RequestJSONDataDict() if self.is_json else RequestDataDict()
3131
self.files = RequestFilesDict()
3232
self.params = RequestQueryParamsDict()
33-
# To preserve the order of fields in file upload multipart requests.
33+
# To preserve the order of fields in file upload multipart niquests.
3434
self.multipart_data = MultipartRequestDataDict()
3535

3636
@classmethod

httpie/client.py

+25-33
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import argparse
2-
import http.client
32
import json
43
import sys
5-
from contextlib import contextmanager
64
from time import monotonic
75
from typing import Any, Dict, Callable, Iterable
86
from urllib.parse import urlparse, urlunparse
97

10-
import requests
8+
import niquests
119
# noinspection PyPackageRequirements
1210
import urllib3
1311
from urllib3.util import SKIP_HEADER, SKIPPABLE_HEADERS
@@ -44,6 +42,7 @@ def collect_messages(
4442
env: Environment,
4543
args: argparse.Namespace,
4644
request_body_read_callback: Callable[[bytes], None] = None,
45+
prepared_request_readiness: Callable[[niquests.PreparedRequest], None] = None,
4746
) -> Iterable[RequestsMessage]:
4847
httpie_session = None
4948
httpie_session_headers = None
@@ -88,7 +87,12 @@ def collect_messages(
8887
# TODO: reflect the split between request and send kwargs.
8988
dump_request(request_kwargs)
9089

91-
request = requests.Request(**request_kwargs)
90+
hooks = None
91+
92+
if prepared_request_readiness:
93+
hooks = {"pre_send": [prepared_request_readiness]}
94+
95+
request = niquests.Request(**request_kwargs, hooks=hooks)
9296
prepared_request = requests_session.prepare_request(request)
9397
transform_headers(request, prepared_request)
9498
if args.path_as_is:
@@ -110,12 +114,13 @@ def collect_messages(
110114
url=prepared_request.url,
111115
**send_kwargs_mergeable_from_env,
112116
)
113-
with max_headers(args.max_headers):
114-
response = requests_session.send(
115-
request=prepared_request,
116-
**send_kwargs_merged,
117-
**send_kwargs,
118-
)
117+
response = requests_session.send(
118+
request=prepared_request,
119+
**send_kwargs_merged,
120+
**send_kwargs,
121+
)
122+
if args.max_headers and len(response.headers) > args.max_headers:
123+
raise niquests.ConnectionError(f"got more than {args.max_headers} headers")
119124
response._httpie_headers_parsed_at = monotonic()
120125
expired_cookies += get_expired_cookies(
121126
response.headers.get('Set-Cookie', '')
@@ -124,7 +129,7 @@ def collect_messages(
124129
response_count += 1
125130
if response.next:
126131
if args.max_redirects and response_count == args.max_redirects:
127-
raise requests.TooManyRedirects
132+
raise niquests.TooManyRedirects
128133
if args.follow:
129134
prepared_request = response.next
130135
if args.all:
@@ -140,25 +145,12 @@ def collect_messages(
140145
httpie_session.save()
141146

142147

143-
# noinspection PyProtectedMember
144-
@contextmanager
145-
def max_headers(limit):
146-
# <https://github.com/httpie/cli/issues/802>
147-
# noinspection PyUnresolvedReferences
148-
orig = http.client._MAXHEADERS
149-
http.client._MAXHEADERS = limit or float('Inf')
150-
try:
151-
yield
152-
finally:
153-
http.client._MAXHEADERS = orig
154-
155-
156148
def build_requests_session(
157149
verify: bool,
158150
ssl_version: str = None,
159151
ciphers: str = None,
160-
) -> requests.Session:
161-
requests_session = requests.Session()
152+
) -> niquests.Session:
153+
requests_session = niquests.Session()
162154

163155
# Install our adapter.
164156
http_adapter = HTTPieHTTPAdapter()
@@ -186,7 +178,7 @@ def build_requests_session(
186178

187179
def dump_request(kwargs: dict):
188180
sys.stderr.write(
189-
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
181+
f'\n>>> niquests.request(**{repr_dict(kwargs)})\n\n')
190182

191183

192184
def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
@@ -210,13 +202,13 @@ def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
210202

211203

212204
def transform_headers(
213-
request: requests.Request,
214-
prepared_request: requests.PreparedRequest
205+
request: niquests.Request,
206+
prepared_request: niquests.PreparedRequest
215207
) -> None:
216208
"""Apply various transformations on top of the `prepared_requests`'s
217209
headers to change the request prepreation behavior."""
218210

219-
# Remove 'Content-Length' when it is misplaced by requests.
211+
# Remove 'Content-Length' when it is misplaced by niquests.
220212
if (
221213
prepared_request.method in IGNORE_CONTENT_LENGTH_METHODS
222214
and prepared_request.headers.get('Content-Length') == '0'
@@ -232,7 +224,7 @@ def transform_headers(
232224

233225
def apply_missing_repeated_headers(
234226
original_headers: HTTPHeadersDict,
235-
prepared_request: requests.PreparedRequest
227+
prepared_request: niquests.PreparedRequest
236228
) -> None:
237229
"""Update the given `prepared_request`'s headers with the original
238230
ones. This allows the requests to be prepared as usual, and then later
@@ -291,7 +283,7 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
291283
cert = args.cert
292284
if args.cert_key:
293285
# Having a client certificate key passphrase is not supported
294-
# by requests. So we are using our own transportation structure
286+
# by niquests. So we are using our own transportation structure
295287
# which is compatible with their format (a tuple of minimum two
296288
# items).
297289
#
@@ -329,7 +321,7 @@ def make_request_kwargs(
329321
request_body_read_callback=lambda chunk: chunk
330322
) -> dict:
331323
"""
332-
Translate our `args` into `requests.Request` keyword arguments.
324+
Translate our `args` into `niquests.Request` keyword arguments.
333325
334326
"""
335327
files = args.files

httpie/core.py

+60-8
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import socket
66
from typing import List, Optional, Union, Callable
77

8-
import requests
8+
import niquests
99
from pygments import __version__ as pygments_version
10-
from requests import __version__ as requests_version
10+
from niquests import __version__ as requests_version
1111

1212
from . import __version__ as httpie_version
1313
from .cli.constants import OUT_REQ_BODY
@@ -112,16 +112,16 @@ def handle_generic_error(e, annotation=None):
112112
if include_traceback:
113113
raise
114114
exit_status = ExitStatus.ERROR
115-
except requests.Timeout:
115+
except niquests.Timeout:
116116
exit_status = ExitStatus.ERROR_TIMEOUT
117117
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
118-
except requests.TooManyRedirects:
118+
except niquests.TooManyRedirects:
119119
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
120120
env.log_error(
121121
f'Too many redirects'
122122
f' (--max-redirects={parsed_args.max_redirects}).'
123123
)
124-
except requests.exceptions.ConnectionError as exc:
124+
except niquests.exceptions.ConnectionError as exc:
125125
annotation = None
126126
original_exc = unwrap_context(exc)
127127
if isinstance(original_exc, socket.gaierror):
@@ -175,8 +175,8 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
175175
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
176176
exit_status = ExitStatus.SUCCESS
177177
downloader = None
178-
initial_request: Optional[requests.PreparedRequest] = None
179-
final_response: Optional[requests.Response] = None
178+
initial_request: Optional[niquests.PreparedRequest] = None
179+
final_response: Optional[niquests.Response] = None
180180
processing_options = ProcessingOptions.from_raw_args(args)
181181

182182
def separate():
@@ -204,8 +204,57 @@ def request_body_read_callback(chunk: bytes):
204204
args.follow = True # --download implies --follow.
205205
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
206206
downloader.pre_request(args.headers)
207+
208+
def prepared_request_readiness(pr):
209+
nonlocal output_options, do_write_body, processing_options
210+
211+
if initial_request == pr:
212+
if args.debug and pr.conn_info and pr.conn_info.destination_address:
213+
sys.stderr.write(
214+
f"""\n>>> Connected to {pr.conn_info.destination_address[0]} port {pr.conn_info.destination_address[1]}\n"""
215+
)
216+
217+
if args.debug and pr.conn_info:
218+
if pr.conn_info.cipher:
219+
sys.stderr.write(
220+
f"""\n>>> Connection secured using {pr.conn_info.tls_version.name.replace('_', '.')} / {pr.conn_info.cipher}\n\n"""
221+
)
222+
223+
if pr.conn_info.certificate_dict:
224+
sys.stderr.write(">>> Server certificate:\n")
225+
226+
if "subject" in pr.conn_info.certificate_dict:
227+
sys.stderr.write(
228+
f">>> subject: {'; '.join([s+'='+v for s, v in pr.conn_info.certificate_dict['subject']])}\n"
229+
)
230+
231+
sys.stderr.write(f">>> start date: {pr.conn_info.certificate_dict['notBefore']}\n")
232+
sys.stderr.write(f">>> expire date: {pr.conn_info.certificate_dict['notAfter']}\n")
233+
234+
if "subjectAltName" in pr.conn_info.certificate_dict:
235+
sys.stderr.write(
236+
f">>> subjectAltName: {'; '.join([s + '=' + v for s, v in pr.conn_info.certificate_dict['subjectAltName']])}\n"
237+
)
238+
239+
sys.stderr.write(
240+
f">>> issuer: {'; '.join([s + '=' + v for s, v in pr.conn_info.certificate_dict['issuer']])}\n\n")
241+
242+
if pr.ocsp_verified is None:
243+
sys.stderr.write(">>> Revocation Status: None\n\n")
244+
else:
245+
sys.stderr.write(f">>> Revocation Status: {'GOOD' if pr.ocsp_verified else 'BAD'}\n\n")
246+
247+
write_message(
248+
requests_message=pr,
249+
env=env,
250+
output_options=output_options._replace(
251+
body=do_write_body
252+
),
253+
processing_options=processing_options
254+
)
255+
207256
messages = collect_messages(env, args=args,
208-
request_body_read_callback=request_body_read_callback)
257+
request_body_read_callback=request_body_read_callback, prepared_request_readiness=prepared_request_readiness)
209258
force_separator = False
210259
prev_with_body = False
211260

@@ -225,6 +274,9 @@ def request_body_read_callback(chunk: bytes):
225274
is_streamed_upload = not isinstance(message.body, (str, bytes))
226275
do_write_body = not is_streamed_upload
227276
force_separator = is_streamed_upload and env.stdout_isatty
277+
if message.conn_info is None and not args.offline:
278+
prev_with_body = output_options.body
279+
continue
228280
else:
229281
final_response = message
230282
if args.check_status or downloader:

httpie/downloads.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import IO, Optional, Tuple
1111
from urllib.parse import urlsplit
1212

13-
import requests
13+
import niquests
1414

1515
from .models import HTTPResponse, OutputOptions
1616
from .output.streams import RawStream
@@ -202,7 +202,7 @@ def pre_request(self, request_headers: dict):
202202
def start(
203203
self,
204204
initial_url: str,
205-
final_response: requests.Response
205+
final_response: niquests.Response
206206
) -> Tuple[RawStream, IO]:
207207
"""
208208
Initiate and return a stream for `response` body with progress
@@ -288,7 +288,7 @@ def chunk_downloaded(self, chunk: bytes):
288288
@staticmethod
289289
def _get_output_file_from_response(
290290
initial_url: str,
291-
final_response: requests.Response,
291+
final_response: niquests.Response,
292292
) -> IO:
293293
# Output file not specified. Pick a name that doesn't exist yet.
294294
filename = None

httpie/internal/update_warnings.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from typing import Any, Optional, Callable
66

7-
import requests
7+
import niquests
88

99
import httpie
1010
from httpie.context import Environment, LogLevel
@@ -41,7 +41,7 @@ def _fetch_updates(env: Environment) -> str:
4141
file = env.config.version_info_file
4242
data = _read_data_error_free(file)
4343

44-
response = requests.get(PACKAGE_INDEX_LINK, verify=False)
44+
response = niquests.get(PACKAGE_INDEX_LINK, verify=False)
4545
response.raise_for_status()
4646

4747
data.setdefault('last_warned_date', None)

httpie/manager/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def generate_subparsers(root, parent_parser, definitions, spec):
133133
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
134134
135135
Be aware that you might be looking for http/https commands for sending
136-
HTTP requests. This command is only available for managing the HTTTPie
136+
HTTP niquests. This command is only available for managing the HTTTPie
137137
plugins and the configuration around it.
138138
'''
139139
),

0 commit comments

Comments
 (0)