You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2025-07-15 01:34:27 +02:00
Implement basic metrics layout & total elapsed time (#1250)
* Initial metadata processing * Dynamic coloring and other stuff * Use -vv / --meta * More testing * Cleanup * Tweek message Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
This commit is contained in:
@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
|
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
|
||||||
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
|
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
|
||||||
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
|
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
|
||||||
|
- Added support for displaying the total elapsed time throguh `--meta`/`-vv` or `--print=m`. ([#243](https://github.com/httpie/httpie/issues/243))
|
||||||
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
|
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
|
||||||
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
|
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
|
||||||
- Broken plugins will no longer crash the whole application. ([#1204](https://github.com/httpie/httpie/issues/1204))
|
- Broken plugins will no longer crash the whole application. ([#1204](https://github.com/httpie/httpie/issues/1204))
|
||||||
|
@ -1497,13 +1497,15 @@ By default, HTTPie only outputs the final response and the whole response
|
|||||||
Content-Length: 477
|
Content-Length: 477
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Date: Sun, 05 Aug 2012 00:25:23 GMT
|
Date: Sun, 05 Aug 2012 00:25:23 GMT
|
||||||
[…]
|
Server: gunicorn/0.13.4
|
||||||
}
|
|
||||||
```
|
{
|
||||||
|
[…]
|
||||||
### Quiet output
|
}
|
||||||
|
```
|
||||||
`--quiet` redirects all output that would otherwise go to `stdout` and `stderr` to `/dev/null` (except for errors and warnings).
|
|
||||||
|
#### Verbosity Level: 2
|
||||||
|
|
||||||
If you run HTTPie with `-vv` or `--verbose --verbose`, then it would also display the response metadata.
|
If you run HTTPie with `-vv` or `--verbose --verbose`, then it would also display the response metadata.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -1516,6 +1518,7 @@ It accepts a string of characters each of which represents a specific part of th
|
|||||||
`--quiet` redirects all output that would otherwise go to `stdout` and `stderr` to `/dev/null` (except for errors and warnings).
|
`--quiet` redirects all output that would otherwise go to `stdout` and `stderr` to `/dev/null` (except for errors and warnings).
|
||||||
This doesn’t affect output to a file via `--output` or `--download`.
|
This doesn’t affect output to a file via `--output` or `--download`.
|
||||||
|
|
||||||
|
```bash
|
||||||
# There will be no output:
|
# There will be no output:
|
||||||
$ http --quiet pie.dev/post enjoy='the silence'
|
$ http --quiet pie.dev/post enjoy='the silence'
|
||||||
```
|
```
|
||||||
@ -1552,6 +1555,15 @@ Server: gunicorn/0.13.4
|
|||||||
As an optimization, the response body is downloaded from the server only if it’s part of the output.
|
As an optimization, the response body is downloaded from the server only if it’s part of the output.
|
||||||
This is similar to performing a `HEAD` request, except that it applies to any HTTP method you use.
|
This is similar to performing a `HEAD` request, except that it applies to any HTTP method you use.
|
||||||
|
|
||||||
|
Let’s say that there is an API that returns the whole resource when it is updated, but you are only interested in the response headers to see the status code after an update:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ http --headers PATCH pie.dev/patch name='New Name'
|
||||||
|
```
|
||||||
|
|
||||||
|
Since you are only printing the HTTP headers here, the connection to the server is closed as soon as all the response headers have been received.
|
||||||
|
Therefore, bandwidth and time isn’t wasted downloading the body which you don’t care about.
|
||||||
|
The response headers are downloaded always, even if they are not part of the output
|
||||||
|
|
||||||
## Raw request body
|
## Raw request body
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from .argtypes import (
|
|||||||
parse_format_options,
|
parse_format_options,
|
||||||
)
|
)
|
||||||
from .constants import (
|
from .constants import (
|
||||||
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
HTTP_GET, HTTP_POST, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||||
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
|
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
|
||||||
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
||||||
SEPARATOR_CREDENTIALS,
|
SEPARATOR_CREDENTIALS,
|
||||||
@ -456,8 +456,10 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
|||||||
self.args.all = True
|
self.args.all = True
|
||||||
|
|
||||||
if self.args.output_options is None:
|
if self.args.output_options is None:
|
||||||
if self.args.verbose:
|
if self.args.verbose >= 2:
|
||||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||||
|
elif self.args.verbose == 1:
|
||||||
|
self.args.output_options = ''.join(BASE_OUTPUT_OPTIONS)
|
||||||
elif self.args.offline:
|
elif self.args.offline:
|
||||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
||||||
elif not self.env.stdout_isatty:
|
elif not self.env.stdout_isatty:
|
||||||
|
@ -73,12 +73,18 @@ OUT_REQ_HEAD = 'H'
|
|||||||
OUT_REQ_BODY = 'B'
|
OUT_REQ_BODY = 'B'
|
||||||
OUT_RESP_HEAD = 'h'
|
OUT_RESP_HEAD = 'h'
|
||||||
OUT_RESP_BODY = 'b'
|
OUT_RESP_BODY = 'b'
|
||||||
|
OUT_RESP_META = 'm'
|
||||||
|
|
||||||
OUTPUT_OPTIONS = frozenset({
|
BASE_OUTPUT_OPTIONS = frozenset({
|
||||||
OUT_REQ_HEAD,
|
OUT_REQ_HEAD,
|
||||||
OUT_REQ_BODY,
|
OUT_REQ_BODY,
|
||||||
OUT_RESP_HEAD,
|
OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY
|
OUT_RESP_BODY,
|
||||||
|
})
|
||||||
|
|
||||||
|
OUTPUT_OPTIONS = frozenset({
|
||||||
|
*BASE_OUTPUT_OPTIONS,
|
||||||
|
OUT_RESP_META,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Pretty
|
# Pretty
|
||||||
|
@ -14,7 +14,7 @@ from .argtypes import (
|
|||||||
from .constants import (
|
from .constants import (
|
||||||
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
||||||
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
OUT_RESP_BODY, OUT_RESP_HEAD, OUT_RESP_META, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||||
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||||
SORTED_FORMAT_OPTIONS_STRING,
|
SORTED_FORMAT_OPTIONS_STRING,
|
||||||
UNSORTED_FORMAT_OPTIONS_STRING,
|
UNSORTED_FORMAT_OPTIONS_STRING,
|
||||||
@ -401,6 +401,16 @@ output_options.add_argument(
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
output_options.add_argument(
|
||||||
|
'--meta', '-m',
|
||||||
|
dest='output_options',
|
||||||
|
action='store_const',
|
||||||
|
const=OUT_RESP_META,
|
||||||
|
help=f'''
|
||||||
|
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
||||||
|
|
||||||
|
'''
|
||||||
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--body', '-b',
|
'--body', '-b',
|
||||||
dest='output_options',
|
dest='output_options',
|
||||||
@ -415,7 +425,8 @@ output_options.add_argument(
|
|||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--verbose', '-v',
|
'--verbose', '-v',
|
||||||
dest='verbose',
|
dest='verbose',
|
||||||
action='store_true',
|
action='count',
|
||||||
|
default=0,
|
||||||
help=f'''
|
help=f'''
|
||||||
Verbose output. Print the whole request as well as the response. Also print
|
Verbose output. Print the whole request as well as the response. Also print
|
||||||
any intermediary requests/responses (such as redirects).
|
any intermediary requests/responses (such as redirects).
|
||||||
|
@ -3,21 +3,20 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
from typing import List, Optional, Tuple, Union, Callable
|
from typing import List, Optional, Union, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pygments import __version__ as pygments_version
|
from pygments import __version__ as pygments_version
|
||||||
from requests import __version__ as requests_version
|
from requests import __version__ as requests_version
|
||||||
|
|
||||||
from . import __version__ as httpie_version
|
from . import __version__ as httpie_version
|
||||||
from .cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
|
from .cli.constants import OUT_REQ_BODY
|
||||||
from .client import collect_messages
|
from .client import collect_messages
|
||||||
from .context import Environment
|
from .context import Environment
|
||||||
from .downloads import Downloader
|
from .downloads import Downloader
|
||||||
from .models import (
|
from .models import (
|
||||||
RequestsMessage,
|
|
||||||
RequestsMessageKind,
|
RequestsMessageKind,
|
||||||
infer_requests_message_kind
|
OutputOptions,
|
||||||
)
|
)
|
||||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
@ -112,9 +111,9 @@ def raw_main(
|
|||||||
original_exc = unwrap_context(exc)
|
original_exc = unwrap_context(exc)
|
||||||
if isinstance(original_exc, socket.gaierror):
|
if isinstance(original_exc, socket.gaierror):
|
||||||
if original_exc.errno == socket.EAI_AGAIN:
|
if original_exc.errno == socket.EAI_AGAIN:
|
||||||
annotation = '\nCouldn\'t connect to a DNS server. Perhaps check your connection and try again.'
|
annotation = '\nCouldn’t connect to a DNS server. Please check your connection and try again.'
|
||||||
elif original_exc.errno == socket.EAI_NONAME:
|
elif original_exc.errno == socket.EAI_NONAME:
|
||||||
annotation = '\nCouldn\'t resolve the given hostname. Perhaps check it and try again.'
|
annotation = '\nCouldn’t resolve the given hostname. Please check the URL and try again.'
|
||||||
propagated_exc = original_exc
|
propagated_exc = original_exc
|
||||||
else:
|
else:
|
||||||
propagated_exc = exc
|
propagated_exc = exc
|
||||||
@ -153,22 +152,6 @@ def main(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_output_options(
|
|
||||||
args: argparse.Namespace,
|
|
||||||
message: RequestsMessage
|
|
||||||
) -> Tuple[bool, bool]:
|
|
||||||
return {
|
|
||||||
RequestsMessageKind.REQUEST: (
|
|
||||||
OUT_REQ_HEAD in args.output_options,
|
|
||||||
OUT_REQ_BODY in args.output_options,
|
|
||||||
),
|
|
||||||
RequestsMessageKind.RESPONSE: (
|
|
||||||
OUT_RESP_HEAD in args.output_options,
|
|
||||||
OUT_RESP_BODY in args.output_options,
|
|
||||||
),
|
|
||||||
}[infer_requests_message_kind(message)]
|
|
||||||
|
|
||||||
|
|
||||||
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||||
"""
|
"""
|
||||||
The main program without error handling.
|
The main program without error handling.
|
||||||
@ -197,7 +180,8 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
msg.is_body_upload_chunk = True
|
msg.is_body_upload_chunk = True
|
||||||
msg.body = chunk
|
msg.body = chunk
|
||||||
msg.headers = initial_request.headers
|
msg.headers = initial_request.headers
|
||||||
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
|
msg_output_options = OutputOptions.from_message(msg, body=True, headers=False)
|
||||||
|
write_message(requests_message=msg, env=env, args=args, output_options=msg_output_options)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.download:
|
if args.download:
|
||||||
@ -211,17 +195,17 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
|
|
||||||
# Process messages as they’re generated
|
# Process messages as they’re generated
|
||||||
for message in messages:
|
for message in messages:
|
||||||
is_request = isinstance(message, requests.PreparedRequest)
|
output_options = OutputOptions.from_message(message, args.output_options)
|
||||||
with_headers, with_body = get_output_options(args=args, message=message)
|
|
||||||
do_write_body = with_body
|
do_write_body = output_options.body
|
||||||
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
|
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty):
|
||||||
# Separate after a previous message with body, if needed. See test_tokens.py.
|
# Separate after a previous message with body, if needed. See test_tokens.py.
|
||||||
separate()
|
separate()
|
||||||
force_separator = False
|
force_separator = False
|
||||||
if is_request:
|
if output_options.kind is RequestsMessageKind.REQUEST:
|
||||||
if not initial_request:
|
if not initial_request:
|
||||||
initial_request = message
|
initial_request = message
|
||||||
if with_body:
|
if output_options.body:
|
||||||
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
||||||
do_write_body = not is_streamed_upload
|
do_write_body = not is_streamed_upload
|
||||||
force_separator = is_streamed_upload and env.stdout_isatty
|
force_separator = is_streamed_upload and env.stdout_isatty
|
||||||
@ -231,9 +215,10 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
||||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
||||||
write_message(requests_message=message, env=env, args=args, with_headers=with_headers,
|
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
|
||||||
with_body=do_write_body)
|
body=do_write_body
|
||||||
prev_with_body = with_body
|
))
|
||||||
|
prev_with_body = output_options.body
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
if force_separator:
|
if force_separator:
|
||||||
|
@ -14,7 +14,7 @@ from urllib.parse import urlsplit
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .models import HTTPResponse
|
from .models import HTTPResponse, OutputOptions
|
||||||
from .output.streams import RawStream
|
from .output.streams import RawStream
|
||||||
from .utils import humanize_bytes
|
from .utils import humanize_bytes
|
||||||
|
|
||||||
@ -266,10 +266,10 @@ class Downloader:
|
|||||||
total_size=total_size
|
total_size=total_size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
||||||
stream = RawStream(
|
stream = RawStream(
|
||||||
msg=HTTPResponse(final_response),
|
msg=HTTPResponse(final_response),
|
||||||
with_headers=False,
|
output_options=output_options,
|
||||||
with_body=True,
|
|
||||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Iterable, Union
|
from typing import Iterable, Union, NamedTuple
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from .utils import split_cookies, parse_content_type_header
|
from .cli.constants import (
|
||||||
|
OUT_REQ_BODY,
|
||||||
|
OUT_REQ_HEAD,
|
||||||
|
OUT_RESP_BODY,
|
||||||
|
OUT_RESP_HEAD,
|
||||||
|
OUT_RESP_META
|
||||||
|
)
|
||||||
from .compat import cached_property
|
from .compat import cached_property
|
||||||
|
from .utils import split_cookies, parse_content_type_header
|
||||||
|
|
||||||
|
|
||||||
class HTTPMessage:
|
class HTTPMessage:
|
||||||
@ -27,6 +34,11 @@ class HTTPMessage:
|
|||||||
"""Return a `str` with the message's headers."""
|
"""Return a `str` with the message's headers."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self) -> str:
|
||||||
|
"""Return metadata about the current message."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def encoding(self) -> str:
|
def encoding(self) -> str:
|
||||||
ct, params = parse_content_type_header(self.content_type)
|
ct, params = parse_content_type_header(self.content_type)
|
||||||
@ -81,6 +93,15 @@ class HTTPResponse(HTTPMessage):
|
|||||||
)
|
)
|
||||||
return '\r\n'.join(headers)
|
return '\r\n'.join(headers)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self) -> str:
|
||||||
|
data = {}
|
||||||
|
data['Elapsed time'] = str(self._orig.elapsed.total_seconds()) + 's'
|
||||||
|
return '\n'.join(
|
||||||
|
f'{key}: {value}'
|
||||||
|
for key, value in data.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HTTPRequest(HTTPMessage):
|
class HTTPRequest(HTTPMessage):
|
||||||
"""A :class:`requests.models.Request` wrapper."""
|
"""A :class:`requests.models.Request` wrapper."""
|
||||||
@ -138,3 +159,50 @@ def infer_requests_message_kind(message: RequestsMessage) -> RequestsMessageKind
|
|||||||
return RequestsMessageKind.RESPONSE
|
return RequestsMessageKind.RESPONSE
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Unexpected message type: {type(message).__name__}")
|
raise TypeError(f"Unexpected message type: {type(message).__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
@ -16,6 +16,7 @@ from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
|||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
from ..lexers.json import EnhancedJsonLexer
|
from ..lexers.json import EnhancedJsonLexer
|
||||||
|
from ..lexers.metadata import MetadataLexer
|
||||||
from ..ui.palette import SHADE_NAMES, get_color
|
from ..ui.palette import SHADE_NAMES, get_color
|
||||||
from ...compat import is_windows
|
from ...compat import is_windows
|
||||||
from ...context import Environment
|
from ...context import Environment
|
||||||
@ -50,6 +51,7 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
group_name = 'colors'
|
group_name = 'colors'
|
||||||
|
metadata_lexer = MetadataLexer()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -68,9 +70,8 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
has_256_colors = env.colors == 256
|
has_256_colors = env.colors == 256
|
||||||
if use_auto_style or not has_256_colors:
|
if use_auto_style or not has_256_colors:
|
||||||
http_lexer = PygmentsHttpLexer()
|
http_lexer = PygmentsHttpLexer()
|
||||||
formatter = TerminalFormatter()
|
body_formatter = header_formatter = TerminalFormatter()
|
||||||
body_formatter = formatter
|
precise = False
|
||||||
header_formatter = formatter
|
|
||||||
else:
|
else:
|
||||||
from ..lexers.http import SimplifiedHTTPLexer
|
from ..lexers.http import SimplifiedHTTPLexer
|
||||||
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
|
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
|
||||||
@ -80,6 +81,7 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
self.header_formatter = header_formatter
|
self.header_formatter = header_formatter
|
||||||
self.body_formatter = body_formatter
|
self.body_formatter = body_formatter
|
||||||
self.http_lexer = http_lexer
|
self.http_lexer = http_lexer
|
||||||
|
self.metadata_lexer = MetadataLexer(precise=precise)
|
||||||
|
|
||||||
def format_headers(self, headers: str) -> str:
|
def format_headers(self, headers: str) -> str:
|
||||||
return pygments.highlight(
|
return pygments.highlight(
|
||||||
@ -98,6 +100,13 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
)
|
)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
def format_metadata(self, metadata: str) -> str:
|
||||||
|
return pygments.highlight(
|
||||||
|
code=metadata,
|
||||||
|
lexer=self.metadata_lexer,
|
||||||
|
formatter=self.header_formatter,
|
||||||
|
).strip()
|
||||||
|
|
||||||
def get_lexer_for_body(
|
def get_lexer_for_body(
|
||||||
self, mime: str,
|
self, mime: str,
|
||||||
body: str
|
body: str
|
||||||
@ -288,6 +297,13 @@ PIE_HEADER_STYLE = {
|
|||||||
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
|
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
|
||||||
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
|
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
|
||||||
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
|
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
pygments.token.Name.Decorator: 'grey',
|
||||||
|
pygments.token.Number.SPEED.FAST: 'bold green',
|
||||||
|
pygments.token.Number.SPEED.AVG: 'bold yellow',
|
||||||
|
pygments.token.Number.SPEED.SLOW: 'bold orange',
|
||||||
|
pygments.token.Number.SPEED.VERY_SLOW: 'bold red',
|
||||||
}
|
}
|
||||||
|
|
||||||
PIE_BODY_STYLE = {
|
PIE_BODY_STYLE = {
|
||||||
|
12
httpie/output/lexers/common.py
Normal file
12
httpie/output/lexers/common.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
def precise(lexer, precise_token, parent_token):
|
||||||
|
# Due to a pygments bug*, custom tokens will look bad
|
||||||
|
# on outside styles. Until it is fixed on upstream, we'll
|
||||||
|
# convey whether the client is using pie style or not
|
||||||
|
# through precise option and return more precise tokens
|
||||||
|
# depending on it's value.
|
||||||
|
#
|
||||||
|
# [0]: https://github.com/pygments/pygments/issues/1986
|
||||||
|
if precise_token is None or not lexer.options.get("precise"):
|
||||||
|
return parent_token
|
||||||
|
else:
|
||||||
|
return precise_token
|
@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import pygments
|
import pygments
|
||||||
|
from httpie.output.lexers.common import precise
|
||||||
|
|
||||||
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
|
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
|
||||||
|
|
||||||
@ -22,20 +22,6 @@ RESPONSE_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def precise(lexer, precise_token, parent_token):
|
|
||||||
# Due to a pygments bug*, custom tokens will look bad
|
|
||||||
# on outside styles. Until it is fixed on upstream, we'll
|
|
||||||
# convey whether the client is using pie style or not
|
|
||||||
# through precise option and return more precise tokens
|
|
||||||
# depending on it's value.
|
|
||||||
#
|
|
||||||
# [0]: https://github.com/pygments/pygments/issues/1986
|
|
||||||
if precise_token is None or not lexer.options.get("precise"):
|
|
||||||
return parent_token
|
|
||||||
else:
|
|
||||||
return precise_token
|
|
||||||
|
|
||||||
|
|
||||||
def http_response_type(lexer, match, ctx):
|
def http_response_type(lexer, match, ctx):
|
||||||
status_match = RE_STATUS_LINE.match(match.group())
|
status_match = RE_STATUS_LINE.match(match.group())
|
||||||
if status_match is None:
|
if status_match is None:
|
||||||
|
57
httpie/output/lexers/metadata.py
Normal file
57
httpie/output/lexers/metadata.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import pygments
|
||||||
|
from httpie.output.lexers.common import precise
|
||||||
|
|
||||||
|
SPEED_TOKENS = {
|
||||||
|
0.45: pygments.token.Number.SPEED.FAST,
|
||||||
|
1.00: pygments.token.Number.SPEED.AVG,
|
||||||
|
2.50: pygments.token.Number.SPEED.SLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def speed_based_token(lexer, match, ctx):
|
||||||
|
try:
|
||||||
|
value = float(match.group())
|
||||||
|
except ValueError:
|
||||||
|
return pygments.token.Number
|
||||||
|
|
||||||
|
for limit, token in SPEED_TOKENS.items():
|
||||||
|
if value <= limit:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
token = pygments.token.Number.SPEED.VERY_SLOW
|
||||||
|
|
||||||
|
response_type = precise(
|
||||||
|
lexer,
|
||||||
|
token,
|
||||||
|
pygments.token.Number
|
||||||
|
)
|
||||||
|
yield match.start(), response_type, match.group()
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataLexer(pygments.lexer.RegexLexer):
|
||||||
|
"""Simple HTTPie metadata lexer."""
|
||||||
|
|
||||||
|
tokens = {
|
||||||
|
'root': [
|
||||||
|
(
|
||||||
|
r'(Elapsed time)( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
|
||||||
|
pygments.token.Name.Decorator, # Name
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Operator, # Colon
|
||||||
|
pygments.token.Text,
|
||||||
|
speed_based_token,
|
||||||
|
pygments.token.Name.Builtin # Value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# Generic item
|
||||||
|
(
|
||||||
|
r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||||
|
pygments.token.Name.Decorator, # Name
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Operator, # Colon
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Text # Value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
@ -51,3 +51,8 @@ class Formatting:
|
|||||||
for p in self.enabled_plugins:
|
for p in self.enabled_plugins:
|
||||||
content = p.format_body(content, mime)
|
content = p.format_body(content, mime)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
def format_metadata(self, metadata: str) -> str:
|
||||||
|
for p in self.enabled_plugins:
|
||||||
|
metadata = p.format_metadata(metadata)
|
||||||
|
return metadata
|
||||||
|
@ -5,7 +5,7 @@ from typing import Callable, Iterable, Optional, Union
|
|||||||
from .processing import Conversion, Formatting
|
from .processing import Conversion, Formatting
|
||||||
from ..context import Environment
|
from ..context import Environment
|
||||||
from ..encoding import smart_decode, smart_encode, UTF8
|
from ..encoding import smart_decode, smart_encode, UTF8
|
||||||
from ..models import HTTPMessage
|
from ..models import HTTPMessage, OutputOptions
|
||||||
from ..utils import parse_content_type_header
|
from ..utils import parse_content_type_header
|
||||||
|
|
||||||
|
|
||||||
@ -33,47 +33,58 @@ class BaseStream(metaclass=ABCMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
msg: HTTPMessage,
|
msg: HTTPMessage,
|
||||||
with_headers=True,
|
output_options: OutputOptions,
|
||||||
with_body=True,
|
|
||||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param msg: a :class:`models.HTTPMessage` subclass
|
:param msg: a :class:`models.HTTPMessage` subclass
|
||||||
:param with_headers: if `True`, headers will be included
|
:param output_options: a :class:`OutputOptions` instance to represent
|
||||||
:param with_body: if `True`, body will be included
|
which parts of the message is printed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert with_headers or with_body
|
assert output_options.any()
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.with_headers = with_headers
|
self.output_options = output_options
|
||||||
self.with_body = with_body
|
|
||||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||||
|
|
||||||
def get_headers(self) -> bytes:
|
def get_headers(self) -> bytes:
|
||||||
"""Return the headers' bytes."""
|
"""Return the headers' bytes."""
|
||||||
return self.msg.headers.encode()
|
return self.msg.headers.encode()
|
||||||
|
|
||||||
|
def get_metadata(self) -> bytes:
|
||||||
|
"""Return the message metadata."""
|
||||||
|
return self.msg.metadata.encode()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def iter_body(self) -> Iterable[bytes]:
|
def iter_body(self) -> Iterable[bytes]:
|
||||||
"""Return an iterator over the message body."""
|
"""Return an iterator over the message body."""
|
||||||
|
|
||||||
def __iter__(self) -> Iterable[bytes]:
|
def __iter__(self) -> Iterable[bytes]:
|
||||||
"""Return an iterator over `self.msg`."""
|
"""Return an iterator over `self.msg`."""
|
||||||
if self.with_headers:
|
if self.output_options.headers:
|
||||||
yield self.get_headers()
|
yield self.get_headers()
|
||||||
yield b'\r\n\r\n'
|
yield b'\r\n\r\n'
|
||||||
|
|
||||||
if self.with_body:
|
if self.output_options.body:
|
||||||
try:
|
try:
|
||||||
for chunk in self.iter_body():
|
for chunk in self.iter_body():
|
||||||
yield chunk
|
yield chunk
|
||||||
if self.on_body_chunk_downloaded:
|
if self.on_body_chunk_downloaded:
|
||||||
self.on_body_chunk_downloaded(chunk)
|
self.on_body_chunk_downloaded(chunk)
|
||||||
except DataSuppressedError as e:
|
except DataSuppressedError as e:
|
||||||
if self.with_headers:
|
if self.output_options.headers:
|
||||||
yield b'\n'
|
yield b'\n'
|
||||||
yield e.message
|
yield e.message
|
||||||
|
|
||||||
|
if self.output_options.meta:
|
||||||
|
mixed = self.output_options.headers or self.output_options.body
|
||||||
|
|
||||||
|
if mixed:
|
||||||
|
yield b'\n\n'
|
||||||
|
|
||||||
|
yield self.get_metadata()
|
||||||
|
if not mixed:
|
||||||
|
yield b'\n'
|
||||||
|
|
||||||
|
|
||||||
class RawStream(BaseStream):
|
class RawStream(BaseStream):
|
||||||
"""The message is streamed in chunks with no processing."""
|
"""The message is streamed in chunks with no processing."""
|
||||||
@ -181,6 +192,10 @@ class PrettyStream(EncodedStream):
|
|||||||
return self.formatting.format_headers(
|
return self.formatting.format_headers(
|
||||||
self.msg.headers).encode(self.output_encoding)
|
self.msg.headers).encode(self.output_encoding)
|
||||||
|
|
||||||
|
def get_metadata(self) -> bytes:
|
||||||
|
return self.formatting.format_metadata(
|
||||||
|
self.msg.metadata).encode(self.output_encoding)
|
||||||
|
|
||||||
def iter_body(self) -> Iterable[bytes]:
|
def iter_body(self) -> Iterable[bytes]:
|
||||||
first_chunk = True
|
first_chunk = True
|
||||||
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
||||||
|
@ -10,7 +10,7 @@ from ..models import (
|
|||||||
HTTPMessage,
|
HTTPMessage,
|
||||||
RequestsMessage,
|
RequestsMessage,
|
||||||
RequestsMessageKind,
|
RequestsMessageKind,
|
||||||
infer_requests_message_kind
|
OutputOptions
|
||||||
)
|
)
|
||||||
from .processing import Conversion, Formatting
|
from .processing import Conversion, Formatting
|
||||||
from .streams import (
|
from .streams import (
|
||||||
@ -26,18 +26,16 @@ def write_message(
|
|||||||
requests_message: RequestsMessage,
|
requests_message: RequestsMessage,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
with_headers=False,
|
output_options: OutputOptions,
|
||||||
with_body=False,
|
|
||||||
):
|
):
|
||||||
if not (with_body or with_headers):
|
if not output_options.any():
|
||||||
return
|
return
|
||||||
write_stream_kwargs = {
|
write_stream_kwargs = {
|
||||||
'stream': build_output_stream_for_message(
|
'stream': build_output_stream_for_message(
|
||||||
args=args,
|
args=args,
|
||||||
env=env,
|
env=env,
|
||||||
requests_message=requests_message,
|
requests_message=requests_message,
|
||||||
with_body=with_body,
|
output_options=output_options,
|
||||||
with_headers=with_headers,
|
|
||||||
),
|
),
|
||||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||||
'outfile': env.stdout,
|
'outfile': env.stdout,
|
||||||
@ -100,13 +98,12 @@ def build_output_stream_for_message(
|
|||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
requests_message: RequestsMessage,
|
requests_message: RequestsMessage,
|
||||||
with_headers: bool,
|
output_options: OutputOptions,
|
||||||
with_body: bool,
|
|
||||||
):
|
):
|
||||||
message_type = {
|
message_type = {
|
||||||
RequestsMessageKind.REQUEST: HTTPRequest,
|
RequestsMessageKind.REQUEST: HTTPRequest,
|
||||||
RequestsMessageKind.RESPONSE: HTTPResponse,
|
RequestsMessageKind.RESPONSE: HTTPResponse,
|
||||||
}[infer_requests_message_kind(requests_message)]
|
}[output_options.kind]
|
||||||
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
||||||
env=env,
|
env=env,
|
||||||
args=args,
|
args=args,
|
||||||
@ -115,11 +112,10 @@ def build_output_stream_for_message(
|
|||||||
)
|
)
|
||||||
yield from stream_class(
|
yield from stream_class(
|
||||||
msg=message_type(requests_message),
|
msg=message_type(requests_message),
|
||||||
with_headers=with_headers,
|
output_options=output_options,
|
||||||
with_body=with_body,
|
|
||||||
**stream_kwargs,
|
**stream_kwargs,
|
||||||
)
|
)
|
||||||
if (env.stdout_isatty and with_body
|
if (env.stdout_isatty and output_options.body
|
||||||
and not getattr(requests_message, 'is_body_upload_chunk', False)):
|
and not getattr(requests_message, 'is_body_upload_chunk', False)):
|
||||||
# Ensure a blank line after the response body.
|
# Ensure a blank line after the response body.
|
||||||
# For terminal output only.
|
# For terminal output only.
|
||||||
|
@ -155,3 +155,11 @@ class FormatterPlugin(BasePlugin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
def format_metadata(self, metadata: str) -> str:
|
||||||
|
"""Return processed `metadata`.
|
||||||
|
|
||||||
|
:param metadata: The metadata as text.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return metadata
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import requests
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ from httpie.downloads import (
|
|||||||
from .utils import http, MockEnvironment
|
from .utils import http, MockEnvironment
|
||||||
|
|
||||||
|
|
||||||
class Response:
|
class Response(requests.Response):
|
||||||
# noinspection PyDefaultArgument
|
# noinspection PyDefaultArgument
|
||||||
def __init__(self, url, headers={}, status_code=200):
|
def __init__(self, url, headers={}, status_code=200):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
7
tests/test_meta.py
Normal file
7
tests/test_meta.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .utils import http
|
||||||
|
|
||||||
|
|
||||||
|
def test_meta_elapsed_time(httpbin, monkeypatch):
|
||||||
|
r = http('--meta', httpbin + '/get')
|
||||||
|
for line in r.splitlines():
|
||||||
|
assert 'Elapsed time' in r
|
@ -17,7 +17,7 @@ from httpie.cli.argtypes import (
|
|||||||
)
|
)
|
||||||
from httpie.cli.definition import parser
|
from httpie.cli.definition import parser
|
||||||
from httpie.encoding import UTF8
|
from httpie.encoding import UTF8
|
||||||
from httpie.output.formatters.colors import get_lexer
|
from httpie.output.formatters.colors import PIE_STYLES, get_lexer
|
||||||
from httpie.status import ExitStatus
|
from httpie.status import ExitStatus
|
||||||
from .fixtures import XML_DATA_RAW, XML_DATA_FORMATTED
|
from .fixtures import XML_DATA_RAW, XML_DATA_FORMATTED
|
||||||
from .utils import COLOR, CRLF, HTTP_OK, MockEnvironment, http, DUMMY_URL
|
from .utils import COLOR, CRLF, HTTP_OK, MockEnvironment, http, DUMMY_URL
|
||||||
@ -227,6 +227,13 @@ def test_ensure_contents_colored(httpbin, endpoint):
|
|||||||
assert COLOR in r
|
assert COLOR in r
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('style', PIE_STYLES.keys())
|
||||||
|
def test_ensure_meta_is_colored(httpbin, style):
|
||||||
|
env = MockEnvironment(colors=256)
|
||||||
|
r = http('--meta', '--style', style, 'GET', httpbin + '/get', env=env)
|
||||||
|
assert COLOR in r
|
||||||
|
|
||||||
|
|
||||||
class TestPrettyOptions:
|
class TestPrettyOptions:
|
||||||
"""Test the --pretty handling."""
|
"""Test the --pretty handling."""
|
||||||
|
|
||||||
|
@ -101,3 +101,18 @@ def test_verbose_chunked(httpbin_with_chunked_support):
|
|||||||
def test_request_headers_response_body(httpbin):
|
def test_request_headers_response_body(httpbin):
|
||||||
r = http('--print=Hb', httpbin + '/get')
|
r = http('--print=Hb', httpbin + '/get')
|
||||||
assert_output_matches(r, ExpectSequence.TERMINAL_REQUEST)
|
assert_output_matches(r, ExpectSequence.TERMINAL_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_single_verbose(httpbin):
|
||||||
|
r = http('-v', httpbin + '/post', 'hello=world')
|
||||||
|
assert_output_matches(r, ExpectSequence.TERMINAL_EXCHANGE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_double_verbose(httpbin):
|
||||||
|
r = http('-vv', httpbin + '/post', 'hello=world')
|
||||||
|
assert_output_matches(r, ExpectSequence.TERMINAL_EXCHANGE_META)
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_meta(httpbin):
|
||||||
|
r = http('--meta', httpbin + '/get')
|
||||||
|
assert_output_matches(r, [Expect.RESPONSE_META])
|
||||||
|
@ -7,6 +7,7 @@ from ...utils import CRLF
|
|||||||
|
|
||||||
|
|
||||||
SEPARATOR_RE = re.compile(f'^{MESSAGE_SEPARATOR}')
|
SEPARATOR_RE = re.compile(f'^{MESSAGE_SEPARATOR}')
|
||||||
|
KEY_VALUE_RE = re.compile(r'[\n]*((.*?):(.+)[\n]?)+[\n]*')
|
||||||
|
|
||||||
|
|
||||||
def make_headers_re(message_type: Expect):
|
def make_headers_re(message_type: Expect):
|
||||||
@ -43,6 +44,7 @@ BODY_ENDINGS = [
|
|||||||
TOKEN_REGEX_MAP = {
|
TOKEN_REGEX_MAP = {
|
||||||
Expect.REQUEST_HEADERS: make_headers_re(Expect.REQUEST_HEADERS),
|
Expect.REQUEST_HEADERS: make_headers_re(Expect.REQUEST_HEADERS),
|
||||||
Expect.RESPONSE_HEADERS: make_headers_re(Expect.RESPONSE_HEADERS),
|
Expect.RESPONSE_HEADERS: make_headers_re(Expect.RESPONSE_HEADERS),
|
||||||
|
Expect.RESPONSE_META: KEY_VALUE_RE,
|
||||||
Expect.SEPARATOR: SEPARATOR_RE,
|
Expect.SEPARATOR: SEPARATOR_RE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +107,29 @@ def test_assert_output_matches_headers_with_body_and_separator():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_assert_output_matches_response_meta():
|
||||||
|
assert_output_matches(
|
||||||
|
(
|
||||||
|
'Key: Value\n'
|
||||||
|
'Elapsed Time: 3.3s'
|
||||||
|
),
|
||||||
|
[Expect.RESPONSE_META]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_assert_output_matches_whole_response():
|
||||||
|
assert_output_matches(
|
||||||
|
(
|
||||||
|
f'HTTP/1.1{CRLF}'
|
||||||
|
f'AAA:BBB{CRLF}'
|
||||||
|
f'{CRLF}'
|
||||||
|
f'CCC{MESSAGE_SEPARATOR}'
|
||||||
|
'Elapsed Time: 3.3s'
|
||||||
|
),
|
||||||
|
[Expect.RESPONSE_HEADERS, Expect.BODY, Expect.RESPONSE_META]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_assert_output_matches_multiple_messages():
|
def test_assert_output_matches_multiple_messages():
|
||||||
assert_output_matches(
|
assert_output_matches(
|
||||||
(
|
(
|
||||||
|
@ -8,6 +8,7 @@ class Expect(Enum):
|
|||||||
"""
|
"""
|
||||||
REQUEST_HEADERS = auto()
|
REQUEST_HEADERS = auto()
|
||||||
RESPONSE_HEADERS = auto()
|
RESPONSE_HEADERS = auto()
|
||||||
|
RESPONSE_META = auto()
|
||||||
BODY = auto()
|
BODY = auto()
|
||||||
SEPARATOR = auto()
|
SEPARATOR = auto()
|
||||||
|
|
||||||
@ -45,6 +46,10 @@ class ExpectSequence:
|
|||||||
*TERMINAL_REQUEST,
|
*TERMINAL_REQUEST,
|
||||||
*TERMINAL_RESPONSE,
|
*TERMINAL_RESPONSE,
|
||||||
]
|
]
|
||||||
|
TERMINAL_EXCHANGE_META = [
|
||||||
|
*TERMINAL_EXCHANGE,
|
||||||
|
Expect.RESPONSE_META
|
||||||
|
]
|
||||||
TERMINAL_BODY = [
|
TERMINAL_BODY = [
|
||||||
RAW_BODY,
|
RAW_BODY,
|
||||||
Expect.SEPARATOR
|
Expect.SEPARATOR
|
||||||
|
Reference in New Issue
Block a user