Skip to content

Commit 52653e4

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

38 files changed

+290
-202
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 }}

docs/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2556,7 +2556,7 @@ HTTPie has the following community channels:
25562556
25572557
Under the hood, HTTPie uses these two amazing libraries:
25582558
2559-
- [Requests](https://requests.readthedocs.io/en/latest/) — Python HTTP library for humans
2559+
- [Niquests](https://niquests.readthedocs.io/en/latest/) — Python HTTP library for humans
25602560
- [Pygments](https://pygments.org/) — Python syntax highlighter
25612561
25622562
#### HTTPie friends

docs/contributors/fetch.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Generate the contributors database.
33
4-
FIXME: replace `requests` calls with the HTTPie API, when available.
4+
FIXME: replace `niquests` calls with the HTTPie API, when available.
55
"""
66
import json
77
import os
@@ -14,7 +14,7 @@
1414
from time import sleep
1515
from typing import Any, Dict, Optional, Set
1616

17-
import requests
17+
import niquests
1818

1919
FullNames = Set[str]
2020
GitHubLogins = Set[str]
@@ -197,10 +197,10 @@ def fetch(url: str, params: Optional[Dict[str, str]] = None) -> UserInfo:
197197
}
198198
for retry in range(1, 6):
199199
debug(f'[{retry}/5]', f'{url = }', f'{params = }')
200-
with requests.get(url, params=params, headers=headers) as req:
200+
with niquests.get(url, params=params, headers=headers) as req:
201201
try:
202202
req.raise_for_status()
203-
except requests.exceptions.HTTPError as exc:
203+
except niquests.exceptions.HTTPError as exc:
204204
if exc.response.status_code == 403:
205205
# 403 Client Error: rate limit exceeded for url: ...
206206
now = int(datetime.utcnow().timestamp())

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/client.py

+24-38
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
@@ -290,12 +282,6 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
290282
if args.cert:
291283
cert = args.cert
292284
if args.cert_key:
293-
# Having a client certificate key passphrase is not supported
294-
# by requests. So we are using our own transportation structure
295-
# which is compatible with their format (a tuple of minimum two
296-
# items).
297-
#
298-
# See: https://github.com/psf/requests/issues/2519
299285
cert = HTTPieCertificate(cert, args.cert_key, args.cert_key_pass.value)
300286

301287
return {
@@ -329,7 +315,7 @@ def make_request_kwargs(
329315
request_body_read_callback=lambda chunk: chunk
330316
) -> dict:
331317
"""
332-
Translate our `args` into `requests.Request` keyword arguments.
318+
Translate our `args` into `niquests.Request` keyword arguments.
333319
334320
"""
335321
files = args.files

httpie/core.py

+97-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,94 @@ 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+
">>> subject: "
229+
)
230+
231+
for entry in pr.conn_info.certificate_dict['subject']:
232+
if len(entry) == 2:
233+
rdns, value = entry
234+
elif len(entry) == 1:
235+
rdns, value = entry[0]
236+
else:
237+
continue
238+
239+
sys.stderr.write(f'{rdns}="{value}"; ')
240+
241+
sys.stderr.write("\n")
242+
243+
sys.stderr.write(f">>> start date: {pr.conn_info.certificate_dict['notBefore']}\n")
244+
sys.stderr.write(f">>> expire date: {pr.conn_info.certificate_dict['notAfter']}\n")
245+
246+
if "subjectAltName" in pr.conn_info.certificate_dict:
247+
sys.stderr.write(
248+
">>> subjectAltName: "
249+
)
250+
251+
for entry in pr.conn_info.certificate_dict['subjectAltName']:
252+
if len(entry) == 2:
253+
rdns, value = entry
254+
sys.stderr.write(f'{rdns}="{value}"; ')
255+
256+
sys.stderr.write("\n")
257+
258+
if "issuer" in pr.conn_info.certificate_dict:
259+
sys.stderr.write(
260+
">>> issuer: "
261+
)
262+
263+
for entry in pr.conn_info.certificate_dict['issuer']:
264+
if len(entry) == 2:
265+
rdns, value = entry
266+
elif len(entry) == 1:
267+
rdns, value = entry[0]
268+
else:
269+
continue
270+
271+
sys.stderr.write(f'{rdns}="{value}"; ')
272+
273+
sys.stderr.write("\n\n")
274+
275+
if pr.ocsp_verified is None:
276+
sys.stderr.write(">>> Revocation status: Unverified\n\n")
277+
elif pr.ocsp_verified:
278+
sys.stderr.write(">>> Revocation status: Good\n\n")
279+
else:
280+
sys.stderr.write(">>> Revocation status: Error\n\n")
281+
else:
282+
sys.stderr.write("\n")
283+
284+
write_message(
285+
requests_message=pr,
286+
env=env,
287+
output_options=output_options._replace(
288+
body=do_write_body
289+
),
290+
processing_options=processing_options
291+
)
292+
207293
messages = collect_messages(env, args=args,
208-
request_body_read_callback=request_body_read_callback)
294+
request_body_read_callback=request_body_read_callback, prepared_request_readiness=prepared_request_readiness)
209295
force_separator = False
210296
prev_with_body = False
211297

@@ -225,6 +311,9 @@ def request_body_read_callback(chunk: bytes):
225311
is_streamed_upload = not isinstance(message.body, (str, bytes))
226312
do_write_body = not is_streamed_upload
227313
force_separator = is_streamed_upload and env.stdout_isatty
314+
if message.conn_info is None and not args.offline:
315+
prev_with_body = output_options.body
316+
continue
228317
else:
229318
final_response = message
230319
if args.check_status or downloader:

0 commit comments

Comments
 (0)