2022-01-23 13:52:38 +01:00
|
|
|
from time import monotonic
|
|
|
|
|
2021-11-25 00:45:39 +01:00
|
|
|
import requests
|
|
|
|
|
|
|
|
from enum import Enum, auto
|
2021-12-23 21:13:25 +01:00
|
|
|
from typing import Iterable, Union, NamedTuple
|
2019-08-29 08:53:56 +02:00
|
|
|
from urllib.parse import urlsplit
|
2012-09-17 00:37:36 +02:00
|
|
|
|
2021-12-23 21:13:25 +01:00
|
|
|
from .cli.constants import (
|
|
|
|
OUT_REQ_BODY,
|
|
|
|
OUT_REQ_HEAD,
|
|
|
|
OUT_RESP_BODY,
|
|
|
|
OUT_RESP_HEAD,
|
|
|
|
OUT_RESP_META
|
|
|
|
)
|
2021-10-06 17:27:07 +02:00
|
|
|
from .compat import cached_property
|
2021-12-23 21:13:25 +01:00
|
|
|
from .utils import split_cookies, parse_content_type_header
|
2021-07-06 21:00:06 +02:00
|
|
|
|
2012-07-21 02:59:43 +02:00
|
|
|
|
2022-01-23 13:52:38 +01:00
|
|
|
ELAPSED_TIME_LABEL = 'Elapsed time'
|
|
|
|
|
|
|
|
|
2021-10-06 17:27:07 +02:00
|
|
|
class HTTPMessage:
|
2012-08-03 01:01:15 +02:00
|
|
|
"""Abstract class for HTTP messages."""
|
2012-07-21 02:59:43 +02:00
|
|
|
|
2012-08-01 21:13:50 +02:00
|
|
|
def __init__(self, orig):
|
|
|
|
self._orig = orig
|
|
|
|
|
2019-08-30 11:32:14 +02:00
|
|
|
def iter_body(self, chunk_size: int) -> Iterable[bytes]:
|
2012-08-03 01:01:15 +02:00
|
|
|
"""Return an iterator over the body."""
|
2021-10-06 17:27:07 +02:00
|
|
|
raise NotImplementedError
|
2012-08-03 01:01:15 +02:00
|
|
|
|
2019-08-30 11:32:14 +02:00
|
|
|
def iter_lines(self, chunk_size: int) -> Iterable[bytes]:
|
2012-08-03 01:01:15 +02:00
|
|
|
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
|
2021-10-06 17:27:07 +02:00
|
|
|
raise NotImplementedError
|
2012-08-03 01:01:15 +02:00
|
|
|
|
|
|
|
@property
|
2019-08-30 11:32:14 +02:00
|
|
|
def headers(self) -> str:
|
2012-08-03 01:01:15 +02:00
|
|
|
"""Return a `str` with the message's headers."""
|
2021-10-06 17:27:07 +02:00
|
|
|
raise NotImplementedError
|
2012-08-03 01:01:15 +02:00
|
|
|
|
2021-12-23 21:13:25 +01:00
|
|
|
@property
|
|
|
|
def metadata(self) -> str:
|
|
|
|
"""Return metadata about the current message."""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2021-10-06 17:27:07 +02:00
|
|
|
@cached_property
|
|
|
|
def encoding(self) -> str:
|
|
|
|
ct, params = parse_content_type_header(self.content_type)
|
|
|
|
return params.get('charset', '')
|
2012-08-03 01:01:15 +02:00
|
|
|
|
2012-08-01 21:13:50 +02:00
|
|
|
@property
|
2019-08-30 11:32:14 +02:00
|
|
|
def content_type(self) -> str:
|
2012-08-03 01:01:15 +02:00
|
|
|
"""Return the message content type."""
|
2014-04-26 17:16:11 +02:00
|
|
|
ct = self._orig.headers.get('Content-Type', '')
|
|
|
|
if not isinstance(ct, str):
|
2021-08-05 20:58:43 +02:00
|
|
|
ct = ct.decode()
|
2014-04-26 17:16:11 +02:00
|
|
|
return ct
|
2012-08-01 21:13:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
class HTTPResponse(HTTPMessage):
|
2012-08-06 22:14:52 +02:00
|
|
|
"""A :class:`requests.models.Response` wrapper."""
|
2012-08-01 21:13:50 +02:00
|
|
|
|
2012-08-03 01:01:15 +02:00
|
|
|
def iter_body(self, chunk_size=1):
|
|
|
|
return self._orig.iter_content(chunk_size=chunk_size)
|
|
|
|
|
|
|
|
def iter_lines(self, chunk_size):
|
2012-08-10 01:07:01 +02:00
|
|
|
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
|
2012-08-01 23:21:52 +02:00
|
|
|
|
2016-03-01 19:53:23 +01:00
|
|
|
# noinspection PyProtectedMember
|
2012-08-01 21:13:50 +02:00
|
|
|
@property
|
2012-08-03 01:01:15 +02:00
|
|
|
def headers(self):
|
2021-07-06 21:00:06 +02:00
|
|
|
try:
|
2022-04-14 16:41:12 +02:00
|
|
|
raw = self._orig.raw
|
|
|
|
if getattr(raw, '_original_response', None):
|
|
|
|
raw_version = raw._original_response.version
|
|
|
|
else:
|
|
|
|
raw_version = raw.version
|
2021-07-06 21:00:06 +02:00
|
|
|
except AttributeError:
|
|
|
|
# Assume HTTP/1.1
|
|
|
|
raw_version = 11
|
2015-02-07 16:29:27 +01:00
|
|
|
version = {
|
|
|
|
9: '0.9',
|
|
|
|
10: '1.0',
|
|
|
|
11: '1.1',
|
2022-04-14 16:41:12 +02:00
|
|
|
20: '2.0',
|
2021-07-06 21:00:06 +02:00
|
|
|
}[raw_version]
|
2015-02-07 16:29:27 +01:00
|
|
|
|
2021-07-06 21:00:06 +02:00
|
|
|
original = self._orig
|
|
|
|
status_line = f'HTTP/{version} {original.status_code} {original.reason}'
|
2012-08-10 01:07:01 +02:00
|
|
|
headers = [status_line]
|
2021-05-27 13:05:41 +02:00
|
|
|
headers.extend(
|
2021-07-06 21:00:06 +02:00
|
|
|
': '.join(header)
|
|
|
|
for header in original.headers.items()
|
|
|
|
if header[0] != 'Set-Cookie'
|
|
|
|
)
|
|
|
|
headers.extend(
|
|
|
|
f'Set-Cookie: {cookie}'
|
2021-11-25 00:41:37 +01:00
|
|
|
for header, value in original.headers.items()
|
|
|
|
for cookie in split_cookies(value)
|
|
|
|
if header == 'Set-Cookie'
|
2021-07-06 21:00:06 +02:00
|
|
|
)
|
2012-08-10 01:07:01 +02:00
|
|
|
return '\r\n'.join(headers)
|
2012-07-28 05:45:44 +02:00
|
|
|
|
2021-12-23 21:13:25 +01:00
|
|
|
@property
|
|
|
|
def metadata(self) -> str:
|
|
|
|
data = {}
|
2022-01-23 13:52:38 +01:00
|
|
|
time_to_parse_headers = self._orig.elapsed.total_seconds()
|
|
|
|
# noinspection PyProtectedMember
|
|
|
|
time_since_headers_parsed = monotonic() - self._orig._httpie_headers_parsed_at
|
|
|
|
time_elapsed = time_to_parse_headers + time_since_headers_parsed
|
|
|
|
# data['Headers time'] = str(round(time_to_parse_headers, 5)) + 's'
|
|
|
|
# data['Body time'] = str(round(time_since_headers_parsed, 5)) + 's'
|
|
|
|
data[ELAPSED_TIME_LABEL] = str(round(time_elapsed, 10)) + 's'
|
2021-12-23 21:13:25 +01:00
|
|
|
return '\n'.join(
|
|
|
|
f'{key}: {value}'
|
|
|
|
for key, value in data.items()
|
|
|
|
)
|
|
|
|
|
2012-08-01 21:13:50 +02:00
|
|
|
|
|
|
|
class HTTPRequest(HTTPMessage):
|
2012-08-06 22:14:52 +02:00
|
|
|
"""A :class:`requests.models.Request` wrapper."""
|
2012-08-01 21:13:50 +02:00
|
|
|
|
2012-08-03 01:01:15 +02:00
|
|
|
def iter_body(self, chunk_size):
|
2012-08-01 23:21:52 +02:00
|
|
|
yield self.body
|
|
|
|
|
2012-08-03 01:01:15 +02:00
|
|
|
def iter_lines(self, chunk_size):
|
|
|
|
yield self.body, b''
|
|
|
|
|
2012-08-01 21:13:50 +02:00
|
|
|
@property
|
2012-08-03 01:01:15 +02:00
|
|
|
def headers(self):
|
2013-01-03 14:12:27 +01:00
|
|
|
url = urlsplit(self._orig.url)
|
2012-07-21 02:59:43 +02:00
|
|
|
|
2012-08-03 01:01:15 +02:00
|
|
|
request_line = '{method} {path}{query} HTTP/1.1'.format(
|
2012-08-01 21:13:50 +02:00
|
|
|
method=self._orig.method,
|
2012-07-21 02:59:43 +02:00
|
|
|
path=url.path or '/',
|
2021-05-25 20:49:07 +02:00
|
|
|
query=f'?{url.query}' if url.query else ''
|
2012-08-03 01:01:15 +02:00
|
|
|
)
|
2012-07-25 14:32:57 +02:00
|
|
|
|
2021-10-31 15:04:39 +01:00
|
|
|
headers = self._orig.headers.copy()
|
2014-06-28 13:24:14 +02:00
|
|
|
if 'Host' not in self._orig.headers:
|
2014-01-06 19:39:11 +01:00
|
|
|
headers['Host'] = url.netloc.split('@')[-1]
|
2012-08-01 21:13:50 +02:00
|
|
|
|
2014-06-28 16:35:57 +02:00
|
|
|
headers = [
|
2021-08-05 20:58:43 +02:00
|
|
|
f'{name}: {value if isinstance(value, str) else value.decode()}'
|
2014-06-28 16:35:57 +02:00
|
|
|
for name, value in headers.items()
|
|
|
|
]
|
2012-08-03 01:01:15 +02:00
|
|
|
|
|
|
|
headers.insert(0, request_line)
|
2014-04-26 20:10:15 +02:00
|
|
|
headers = '\r\n'.join(headers).strip()
|
|
|
|
return headers
|
2012-08-01 21:13:50 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def body(self):
|
2013-01-03 14:54:34 +01:00
|
|
|
body = self._orig.body
|
|
|
|
if isinstance(body, str):
|
|
|
|
# Happens with JSON/form request data parsed from the command line.
|
2021-08-05 20:58:43 +02:00
|
|
|
body = body.encode()
|
2013-01-03 14:54:34 +01:00
|
|
|
return body or b''
|
2021-11-25 00:45:39 +01:00
|
|
|
|
|
|
|
|
|
|
|
RequestsMessage = Union[requests.PreparedRequest, requests.Response]
|
|
|
|
|
|
|
|
|
|
|
|
class RequestsMessageKind(Enum):
|
|
|
|
REQUEST = auto()
|
|
|
|
RESPONSE = auto()
|
|
|
|
|
|
|
|
|
|
|
|
def infer_requests_message_kind(message: RequestsMessage) -> RequestsMessageKind:
|
|
|
|
if isinstance(message, requests.PreparedRequest):
|
|
|
|
return RequestsMessageKind.REQUEST
|
|
|
|
elif isinstance(message, requests.Response):
|
|
|
|
return RequestsMessageKind.RESPONSE
|
|
|
|
else:
|
|
|
|
raise TypeError(f"Unexpected message type: {type(message).__name__}")
|
2021-12-23 21:13:25 +01:00
|
|
|
|
|
|
|
|
|
|
|
OPTION_TO_PARAM = {
|
|
|
|
RequestsMessageKind.REQUEST: {
|
|
|
|
'headers': OUT_REQ_HEAD,
|
|
|
|
'body': OUT_REQ_BODY,
|
|
|
|
},
|
|
|
|
RequestsMessageKind.RESPONSE: {
|
|
|
|
'headers': OUT_RESP_HEAD,
|
|
|
|
'body': OUT_RESP_BODY,
|
|
|
|
'meta': OUT_RESP_META
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class OutputOptions(NamedTuple):
|
|
|
|
kind: RequestsMessageKind
|
|
|
|
headers: bool
|
|
|
|
body: bool
|
|
|
|
meta: bool = False
|
|
|
|
|
|
|
|
def any(self):
|
|
|
|
return (
|
|
|
|
self.headers
|
|
|
|
or self.body
|
|
|
|
or self.meta
|
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_message(
|
|
|
|
cls,
|
|
|
|
message: RequestsMessage,
|
|
|
|
raw_args: str = '',
|
|
|
|
**kwargs
|
|
|
|
):
|
|
|
|
kind = infer_requests_message_kind(message)
|
|
|
|
|
|
|
|
options = {
|
|
|
|
option: param in raw_args
|
|
|
|
for option, param in OPTION_TO_PARAM[kind].items()
|
|
|
|
}
|
|
|
|
options.update(kwargs)
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
kind=kind,
|
|
|
|
**options
|
|
|
|
)
|