Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working towards HTTP/2 - httpcore approach. #972

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 82 additions & 0 deletions httpie/adapters.py
@@ -0,0 +1,82 @@
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import parse_url
from urllib3.response import HTTPResponse
from httpcore import SyncConnectionPool, PlainByteStream


class FileLike:
def __init__(self, bytestream):
self._bytestream = iter(bytestream)
self.closed = False

def read(self, amt: int = None):
return next(self._bytestream, b"")

def close(self):
self._bytestream.close()
self.closed = True


class StreamLike:
def __init__(self, filelike):
self._filelike = filelike

def __iter__(self):
chunk = self._filelike.read(4096)
while chunk:
yield chunk
chunk = self._filelike.read(4096)

def close():
pass


class HTTPCoreAdapter(HTTPAdapter):
def __init__(self):
self.pool = SyncConnectionPool()

def send(self, request, stream=False, timeout=None, verify=True,
cert=None, proxies=None):
method = request.method.encode("ascii")

parsed = parse_url(request.url)
scheme = parsed.scheme.encode("ascii")
host = parsed.host.encode("ascii")
port = parsed.port
target = parsed.path.encode("ascii")
if parsed.query:
target += b"?" + parsed.query.encode("ascii")
url = (scheme, host, port, target)

headers = [(b'Host', parsed.netloc.encode("ascii"))]
headers.extend([
(key, val) for key, val in request.headers.items()
])

if not request.body:
stream = PlainByteStream(b"")
elif isinstance(request.body, str):
stream = PlainByteStream(request.body.encode("utf-8"))
elif isinstance(request.body, bytes):
stream = PlainByteStream(request.body)
else:
stream = StreamLike(request.body)

ext = {}

status_code, headers, stream, ext = self.pool.request(method, url, headers, stream, ext)

urllib3_response = HTTPResponse(
body=FileLike(stream),
headers=[(key.decode("ascii"), val.decode("ascii")) for key, val in headers],
status=status_code,
reason=ext['reason'],
version={'HTTP/0.9': 9, 'HTTP/1.0': 10, 'HTTP/1.1': 11, 'HTTP/2': 20}[ext['http_version']],
preload_content=False,
)

return self.build_response(request, urllib3_response)

def close(self):
self.pool.close()
39 changes: 21 additions & 18 deletions httpie/client.py
Expand Up @@ -12,6 +12,7 @@
# noinspection PyPackageRequirements
import urllib3
from httpie import __version__
from httpie.adapters import HTTPCoreAdapter
from httpie.cli.dicts import RequestHeadersDict
from httpie.plugins.registry import plugin_manager
from httpie.sessions import get_httpie_session
Expand Down Expand Up @@ -99,7 +100,7 @@ def collect_messages(

# noinspection PyProtectedMember
expired_cookies += get_expired_cookies(
headers=response.raw._original_response.msg._headers
headers=response.raw.headers.items()
)

response_count += 1
Expand Down Expand Up @@ -160,24 +161,26 @@ def build_requests_session(
) -> requests.Session:
requests_session = requests.Session()

requests_session.mount("http://", HTTPCoreAdapter())
requests_session.mount("https://", HTTPCoreAdapter())
# Install our adapter.
https_adapter = HTTPieHTTPSAdapter(
ciphers=ciphers,
verify=verify,
ssl_version=(
AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
if ssl_version else None
),
)
requests_session.mount('https://', https_adapter)

# Install adapters from plugins.
for plugin_cls in plugin_manager.get_transport_plugins():
transport_plugin = plugin_cls()
requests_session.mount(
prefix=transport_plugin.prefix,
adapter=transport_plugin.get_adapter(),
)
# https_adapter = HTTPieHTTPSAdapter(
# ciphers=ciphers,
# verify=verify,
# ssl_version=(
# AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
# if ssl_version else None
# ),
# )
# requests_session.mount('https://', https_adapter)
#
# # Install adapters from plugins.
# for plugin_cls in plugin_manager.get_transport_plugins():
# transport_plugin = plugin_cls()
# requests_session.mount(
# prefix=transport_plugin.prefix,
# adapter=transport_plugin.get_adapter(),
# )

return requests_session

Expand Down
10 changes: 5 additions & 5 deletions httpie/models.py
Expand Up @@ -52,26 +52,26 @@ def iter_lines(self, chunk_size):
# noinspection PyProtectedMember
@property
def headers(self):
original = self._orig.raw._original_response
raw = self._orig.raw

version = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2',
}[original.version]
}[raw.version]

status_line = f'HTTP/{version} {original.status} {original.reason}'
status_line = f'HTTP/{version} {raw.status} {raw.reason}'
headers = [status_line]
try:
# `original.msg` is a `http.client.HTTPMessage` on Python 3
# `_headers` is a 2-tuple
headers.extend(
'%s: %s' % header for header in original.msg._headers)
'%s: %s' % header for header in raw.headers.items())
except AttributeError:
# and a `httplib.HTTPMessage` on Python 2.x
# `headers` is a list of `name: val<CRLF>`.
headers.extend(h.strip() for h in original.msg.headers)
headers.extend(h.strip() for h in raw.headers)

return '\r\n'.join(headers)

Expand Down
2 changes: 1 addition & 1 deletion tests/utils.py
Expand Up @@ -133,7 +133,7 @@ def json(self) -> Optional[dict]:
elif self.strip().startswith('{'):
# Looks like JSON body.
self._json = json.loads(self)
elif (self.count('Content-Type:') == 1
elif (self.count('content-type:') == 1
and 'application/json' in self):
# Looks like a whole JSON HTTP message,
# try to extract its body.
Expand Down