mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 09:13:25 +01: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:
parent
e0e03f3237
commit
f3b500119c
@ -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))
|
||||||
|
@ -1498,10 +1498,12 @@ message is printed (headers as well as the body). You can control what should
|
|||||||
be printed via several options:
|
be printed via several options:
|
||||||
|
|
||||||
| Option | What is printed |
|
| Option | What is printed |
|
||||||
| --------------: | -------------------------------------------------------------------------------------------------- |
|
| -------------------------: | -------------------------------------------------------------------------------------------------- |
|
||||||
| `--headers, -h` | Only the response headers are printed |
|
| `--headers, -h` | Only the response headers are printed |
|
||||||
| `--body, -b` | Only the response body is printed |
|
| `--body, -b` | Only the response body is printed |
|
||||||
|
| `--meta, -m` | Only the response metadata is printed (various metrics like total elapsed time) |
|
||||||
| `--verbose, -v` | Print the whole HTTP exchange (request and response). This option also enables `--all` (see below) |
|
| `--verbose, -v` | Print the whole HTTP exchange (request and response). This option also enables `--all` (see below) |
|
||||||
|
| `--verbose --verbose, -vv` | Just like `-v`, but also include the response metadata. |
|
||||||
| `--print, -p` | Selects parts of the HTTP exchange |
|
| `--print, -p` | Selects parts of the HTTP exchange |
|
||||||
| `--quiet, -q` | Don't print anything to `stdout` and `stderr` |
|
| `--quiet, -q` | Don't print anything to `stdout` and `stderr` |
|
||||||
|
|
||||||
@ -1516,6 +1518,7 @@ It accepts a string of characters each of which represents a specific part of th
|
|||||||
| `B` | request body |
|
| `B` | request body |
|
||||||
| `h` | response headers |
|
| `h` | response headers |
|
||||||
| `b` | response body |
|
| `b` | response body |
|
||||||
|
| `m` | response meta |
|
||||||
|
|
||||||
Print request and response headers:
|
Print request and response headers:
|
||||||
|
|
||||||
@ -1552,6 +1555,15 @@ Server: gunicorn/0.13.4
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Verbosity Level: 2
|
||||||
|
|
||||||
|
If you run HTTPie with `-vv` or `--verbose --verbose`, then it would also display the response metadata.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Just like the above, but with additional columns like the total elapsed time
|
||||||
|
$ http -vv pie.dev/get
|
||||||
|
```
|
||||||
|
|
||||||
### Quiet output
|
### Quiet output
|
||||||
|
|
||||||
`--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).
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user