httpie-cli/httpie/core.py

278 lines
8.5 KiB
Python
Raw Normal View History

2012-07-26 06:37:03 +02:00
"""This module provides the main functionality of HTTPie.
Invocation flow:
1. Read, validate and process the input (args, `stdin`).
2. Create and send a request.
3. Stream, and possibly process and format, the parts
of the request-response exchange selected by output options.
4. Simultaneously write to `stdout`
5. Exit.
2012-07-26 06:37:03 +02:00
"""
import argparse
import errno
2016-03-04 18:42:13 +01:00
import platform
import sys
from typing import Callable, List, Union
2012-07-26 00:26:23 +02:00
import requests
2012-08-07 14:50:51 +02:00
from pygments import __version__ as pygments_version
from requests import __version__ as requests_version
2012-07-26 00:26:23 +02:00
from httpie import ExitStatus, __version__ as httpie_version
from httpie.client import get_response
from httpie.context import Environment
from httpie.downloads import Downloader
from httpie.output.streams import (
build_output_stream,
2016-02-29 08:00:17 +01:00
write_stream,
write_stream_with_colors_win_py3,
)
from httpie.plugins import plugin_manager
def get_exit_status(http_status: int, follow=False) -> ExitStatus:
2012-12-05 05:03:18 +01:00
"""Translate HTTP status code to exit status code."""
2012-12-11 12:54:34 +01:00
if 300 <= http_status <= 399 and not follow:
# Redirect
2012-12-05 05:03:18 +01:00
return ExitStatus.ERROR_HTTP_3XX
2012-12-11 12:54:34 +01:00
elif 400 <= http_status <= 499:
# Client Error
2012-12-05 05:03:18 +01:00
return ExitStatus.ERROR_HTTP_4XX
2012-12-11 12:54:34 +01:00
elif 500 <= http_status <= 599:
# Server Error
2012-12-05 05:03:18 +01:00
return ExitStatus.ERROR_HTTP_5XX
else:
return ExitStatus.SUCCESS
def print_debug_info(env: Environment):
2014-04-24 18:32:15 +02:00
env.stderr.writelines([
'HTTPie %s\n' % httpie_version,
'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version,
2016-03-04 18:42:13 +01:00
'Python %s\n%s\n' % (sys.version, sys.executable),
'%s %s' % (platform.system(), platform.release()),
])
2016-03-04 18:42:13 +01:00
env.stderr.write('\n\n')
env.stderr.write(repr(env))
env.stderr.write('\n')
def decode_args(
args: List[Union[str, bytes]],
stdin_encoding: str
) -> List[str]:
"""
2016-07-27 02:54:26 +02:00
Convert all bytes args to str
by decoding them using stdin encoding.
"""
return [
2014-06-03 19:44:22 +02:00
arg.decode(stdin_encoding)
if type(arg) == bytes else arg
for arg in args
]
def program(
args: argparse.Namespace,
env: Environment,
log_error: Callable
) -> ExitStatus:
"""
The main program without error handling
2013-09-21 23:46:15 +02:00
:param args: parsed args (argparse.Namespace)
:type env: Environment
:param log_error: error log function
:return: status code
2014-06-03 19:44:22 +02:00
"""
exit_status = ExitStatus.SUCCESS
2016-02-29 08:00:17 +01:00
downloader = None
show_traceback = args.debug or args.traceback
2012-09-17 02:15:00 +02:00
try:
if args.download:
2013-03-24 15:23:18 +01:00
args.follow = True # --download implies --follow.
2016-02-29 08:00:17 +01:00
downloader = Downloader(
output_file=args.output_file,
progress_file=env.stderr,
resume=args.download_resume
)
2016-02-29 08:00:17 +01:00
downloader.pre_request(args.headers)
final_response = get_response(args, config_dir=env.config.directory)
if args.all:
responses = final_response.history + [final_response]
else:
responses = [final_response]
for response in responses:
2016-02-29 08:00:17 +01:00
if args.check_status or downloader:
2016-02-29 05:56:40 +01:00
exit_status = get_exit_status(
http_status=response.status_code,
follow=args.follow
)
if not env.stdout_isatty and exit_status != ExitStatus.SUCCESS:
log_error(
'HTTP %s %s', response.raw.status, response.raw.reason,
level='warning'
)
2016-02-29 05:56:40 +01:00
2016-02-29 08:00:17 +01:00
write_stream_kwargs = {
'stream': build_output_stream(
args=args,
env=env,
request=response.request,
response=response,
output_options=(
args.output_options
if response is final_response
else args.output_options_history
)
2016-02-29 08:00:17 +01:00
),
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
2016-02-29 05:56:40 +01:00
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
2019-08-29 08:53:56 +02:00
if env.is_windows and 'colors' in args.prettify:
2016-02-29 08:00:17 +01:00
write_stream_with_colors_win_py3(**write_stream_kwargs)
2016-02-29 05:56:40 +01:00
else:
2016-02-29 08:00:17 +01:00
write_stream(**write_stream_kwargs)
2016-02-29 05:56:40 +01:00
except IOError as e:
if not show_traceback and e.errno == errno.EPIPE:
2016-02-29 05:56:40 +01:00
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
if downloader and exit_status == ExitStatus.SUCCESS:
2016-02-29 05:56:40 +01:00
# Last response body download.
download_stream, download_to = downloader.start(final_response)
2016-02-29 08:00:17 +01:00
write_stream(
2016-02-29 05:56:40 +01:00
stream=download_stream,
outfile=download_to,
flush=False,
)
2016-02-29 08:00:17 +01:00
downloader.finish()
if downloader.interrupted:
2016-02-29 05:56:40 +01:00
exit_status = ExitStatus.ERROR
log_error('Incomplete download: size=%d; downloaded=%d' % (
2016-02-29 08:00:17 +01:00
downloader.status.total_size,
downloader.status.downloaded
2016-02-29 05:56:40 +01:00
))
return exit_status
finally:
if downloader and not downloader.finished:
downloader.failed()
if (not isinstance(args, list) and args.output_file
and args.output_file_specified):
args.output_file.close()
def main(
args: List[Union[str, bytes]] = sys.argv,
env=Environment(),
custom_log_error: Callable = None
) -> ExitStatus:
"""
The main function.
2016-03-01 14:37:26 +01:00
Pre-process args, handle some special types of invocations,
and run the main program with error handling.
Return exit status code.
"""
args = decode_args(args, env.stdin_encoding)
program_name, *args = args
plugin_manager.load_installed_plugins()
2016-03-01 14:27:26 +01:00
def log_error(msg, *args, **kwargs):
msg = msg % args
2016-03-01 14:27:26 +01:00
level = kwargs.get('level', 'error')
assert level in ['error', 'warning']
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
from httpie.cli import parser
if env.config.default_options:
args = env.config.default_options + args
if custom_log_error:
log_error = custom_log_error
include_debug_info = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args
if include_debug_info:
print_debug_info(env)
if args == ['--debug']:
return ExitStatus.SUCCESS
exit_status = ExitStatus.SUCCESS
try:
parsed_args = parser.parse_args(
args=args,
program_name=program_name,
env=env,
)
except KeyboardInterrupt:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.SUCCESS.value:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
else:
try:
exit_status = program(
args=parsed_args,
env=env,
log_error=log_error,
)
except KeyboardInterrupt:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.SUCCESS.value:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
log_error('Request timed out (%ss).', parsed_args.timeout)
except requests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
log_error('Too many redirects (--max-redirects=%s).',
2016-07-19 18:23:40 +02:00
parsed_args.max_redirects)
except Exception as e:
# TODO: Further distinction between expected and unexpected errors.
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg += ' while doing %s request to URL: %s' % (
request.method, request.url)
log_error('%s: %s', type(e).__name__, msg)
if include_traceback:
raise
exit_status = ExitStatus.ERROR
2012-12-11 12:54:34 +01:00
return exit_status