2019-08-30 11:32:14 +02:00
|
|
|
|
import argparse
|
2019-12-02 17:43:16 +01:00
|
|
|
|
import os
|
2016-03-04 18:42:13 +01:00
|
|
|
|
import platform
|
2019-08-30 11:32:14 +02:00
|
|
|
|
import sys
|
2021-12-23 20:35:30 +01:00
|
|
|
|
import socket
|
2021-11-30 09:12:51 +01:00
|
|
|
|
from typing import List, Optional, Tuple, Union, Callable
|
2012-07-26 00:26:23 +02:00
|
|
|
|
|
2012-07-21 02:59:43 +02:00
|
|
|
|
import requests
|
2012-08-07 14:50:51 +02:00
|
|
|
|
from pygments import __version__ as pygments_version
|
2019-08-30 11:32:14 +02:00
|
|
|
|
from requests import __version__ as requests_version
|
2012-07-26 00:26:23 +02:00
|
|
|
|
|
2021-05-05 14:13:39 +02:00
|
|
|
|
from . import __version__ as httpie_version
|
|
|
|
|
from .cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
|
|
|
|
|
from .client import collect_messages
|
|
|
|
|
from .context import Environment
|
|
|
|
|
from .downloads import Downloader
|
2021-11-25 00:45:39 +01:00
|
|
|
|
from .models import (
|
|
|
|
|
RequestsMessage,
|
|
|
|
|
RequestsMessageKind,
|
|
|
|
|
infer_requests_message_kind
|
|
|
|
|
)
|
2021-05-05 14:13:39 +02:00
|
|
|
|
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
|
|
|
|
from .plugins.registry import plugin_manager
|
|
|
|
|
from .status import ExitStatus, http_status_to_exit_status
|
2021-12-23 20:35:30 +01:00
|
|
|
|
from .utils import unwrap_context
|
2012-07-21 02:59:43 +02:00
|
|
|
|
|
|
|
|
|
|
2020-05-26 10:07:34 +02:00
|
|
|
|
# noinspection PyDefaultArgument
|
2021-11-30 09:12:51 +01:00
|
|
|
|
def raw_main(
|
|
|
|
|
parser: argparse.ArgumentParser,
|
|
|
|
|
main_program: Callable[[argparse.Namespace, Environment], ExitStatus],
|
|
|
|
|
args: List[Union[str, bytes]] = sys.argv,
|
|
|
|
|
env: Environment = Environment()
|
|
|
|
|
) -> ExitStatus:
|
2019-08-29 13:08:02 +02:00
|
|
|
|
program_name, *args = args
|
2019-12-02 17:43:16 +01:00
|
|
|
|
env.program_name = os.path.basename(program_name)
|
|
|
|
|
args = decode_raw_args(args, env.stdin_encoding)
|
2021-11-30 09:12:51 +01:00
|
|
|
|
plugin_manager.load_installed_plugins(env.config.plugins_dir)
|
2016-03-01 14:10:54 +01:00
|
|
|
|
|
|
|
|
|
if env.config.default_options:
|
|
|
|
|
args = env.config.default_options + args
|
|
|
|
|
|
|
|
|
|
include_debug_info = '--debug' in args
|
|
|
|
|
include_traceback = include_debug_info or '--traceback' in args
|
|
|
|
|
|
2021-12-23 20:35:30 +01:00
|
|
|
|
def handle_generic_error(e, annotation=None):
|
|
|
|
|
msg = str(e)
|
|
|
|
|
if hasattr(e, 'request'):
|
|
|
|
|
request = e.request
|
|
|
|
|
if hasattr(request, 'url'):
|
|
|
|
|
msg = (
|
|
|
|
|
f'{msg} while doing a {request.method}'
|
|
|
|
|
f' request to URL: {request.url}'
|
|
|
|
|
)
|
|
|
|
|
if annotation:
|
|
|
|
|
msg += annotation
|
|
|
|
|
env.log_error(f'{type(e).__name__}: {msg}')
|
|
|
|
|
if include_traceback:
|
|
|
|
|
raise
|
|
|
|
|
|
2016-03-01 14:10:54 +01:00
|
|
|
|
if include_debug_info:
|
|
|
|
|
print_debug_info(env)
|
|
|
|
|
if args == ['--debug']:
|
2018-11-02 16:07:39 +01:00
|
|
|
|
return ExitStatus.SUCCESS
|
2016-03-01 14:10:54 +01:00
|
|
|
|
|
2018-11-02 16:07:39 +01:00
|
|
|
|
exit_status = ExitStatus.SUCCESS
|
2016-03-01 14:10:54 +01:00
|
|
|
|
|
|
|
|
|
try:
|
2019-08-29 13:08:02 +02:00
|
|
|
|
parsed_args = parser.parse_args(
|
|
|
|
|
args=args,
|
|
|
|
|
env=env,
|
|
|
|
|
)
|
2015-01-19 15:39:46 +01:00
|
|
|
|
except KeyboardInterrupt:
|
2012-08-01 23:21:52 +02:00
|
|
|
|
env.stderr.write('\n')
|
2016-03-01 14:10:54 +01:00
|
|
|
|
if include_traceback:
|
|
|
|
|
raise
|
2016-10-26 11:53:01 +02:00
|
|
|
|
exit_status = ExitStatus.ERROR_CTRL_C
|
2015-01-19 15:39:46 +01:00
|
|
|
|
except SystemExit as e:
|
2019-09-18 11:57:06 +02:00
|
|
|
|
if e.code != ExitStatus.SUCCESS:
|
2016-03-01 14:10:54 +01:00
|
|
|
|
env.stderr.write('\n')
|
|
|
|
|
if include_traceback:
|
2015-01-19 15:39:46 +01:00
|
|
|
|
raise
|
2016-03-01 14:10:54 +01:00
|
|
|
|
exit_status = ExitStatus.ERROR
|
|
|
|
|
else:
|
|
|
|
|
try:
|
2021-11-30 09:12:51 +01:00
|
|
|
|
exit_status = main_program(
|
2016-03-01 14:10:54 +01:00
|
|
|
|
args=parsed_args,
|
|
|
|
|
env=env,
|
|
|
|
|
)
|
|
|
|
|
except KeyboardInterrupt:
|
2015-01-19 15:39:46 +01:00
|
|
|
|
env.stderr.write('\n')
|
2016-03-01 14:10:54 +01:00
|
|
|
|
if include_traceback:
|
|
|
|
|
raise
|
2016-10-26 11:53:01 +02:00
|
|
|
|
exit_status = ExitStatus.ERROR_CTRL_C
|
2016-03-01 14:10:54 +01:00
|
|
|
|
except SystemExit as e:
|
2019-09-18 11:57:06 +02:00
|
|
|
|
if e.code != ExitStatus.SUCCESS:
|
2016-03-01 14:10:54 +01:00
|
|
|
|
env.stderr.write('\n')
|
|
|
|
|
if include_traceback:
|
|
|
|
|
raise
|
|
|
|
|
exit_status = ExitStatus.ERROR
|
|
|
|
|
except requests.Timeout:
|
|
|
|
|
exit_status = ExitStatus.ERROR_TIMEOUT
|
2019-12-02 17:43:16 +01:00
|
|
|
|
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
|
2016-03-01 14:10:54 +01:00
|
|
|
|
except requests.TooManyRedirects:
|
|
|
|
|
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
2019-12-02 17:43:16 +01:00
|
|
|
|
env.log_error(
|
2019-09-03 17:14:39 +02:00
|
|
|
|
f'Too many redirects'
|
2020-04-15 17:43:08 +02:00
|
|
|
|
f' (--max-redirects={parsed_args.max_redirects}).'
|
2019-09-03 17:14:39 +02:00
|
|
|
|
)
|
2021-12-23 20:35:30 +01:00
|
|
|
|
except requests.exceptions.ConnectionError as exc:
|
|
|
|
|
annotation = None
|
|
|
|
|
original_exc = unwrap_context(exc)
|
|
|
|
|
if isinstance(original_exc, socket.gaierror):
|
|
|
|
|
if original_exc.errno == socket.EAI_AGAIN:
|
|
|
|
|
annotation = '\nCouldn\'t connect to a DNS server. Perhaps check your connection and try again.'
|
|
|
|
|
elif original_exc.errno == socket.EAI_NONAME:
|
|
|
|
|
annotation = '\nCouldn\'t resolve the given hostname. Perhaps check it and try again.'
|
|
|
|
|
propagated_exc = original_exc
|
|
|
|
|
else:
|
|
|
|
|
propagated_exc = exc
|
|
|
|
|
|
|
|
|
|
handle_generic_error(propagated_exc, annotation=annotation)
|
|
|
|
|
exit_status = ExitStatus.ERROR
|
2016-03-01 14:10:54 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
# TODO: Further distinction between expected and unexpected errors.
|
2021-12-23 20:35:30 +01:00
|
|
|
|
handle_generic_error(e)
|
2015-01-19 15:39:46 +01:00
|
|
|
|
exit_status = ExitStatus.ERROR
|
2016-03-01 13:24:50 +01:00
|
|
|
|
|
2012-12-11 12:54:34 +01:00
|
|
|
|
return exit_status
|
2019-09-03 17:14:39 +02:00
|
|
|
|
|
|
|
|
|
|
2021-11-30 09:12:51 +01:00
|
|
|
|
def main(
|
|
|
|
|
args: List[Union[str, bytes]] = sys.argv,
|
|
|
|
|
env: Environment = Environment()
|
|
|
|
|
) -> ExitStatus:
|
|
|
|
|
"""
|
|
|
|
|
The main function.
|
|
|
|
|
|
|
|
|
|
Pre-process args, handle some special types of invocations,
|
|
|
|
|
and run the main program with error handling.
|
|
|
|
|
|
|
|
|
|
Return exit status code.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from .cli.definition import parser
|
|
|
|
|
|
|
|
|
|
return raw_main(
|
|
|
|
|
parser=parser,
|
|
|
|
|
main_program=program,
|
|
|
|
|
args=args,
|
|
|
|
|
env=env
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2020-09-25 13:44:28 +02:00
|
|
|
|
def get_output_options(
|
|
|
|
|
args: argparse.Namespace,
|
2021-11-25 00:45:39 +01:00
|
|
|
|
message: RequestsMessage
|
2020-09-28 12:16:57 +02:00
|
|
|
|
) -> Tuple[bool, bool]:
|
2020-09-25 13:44:28 +02:00
|
|
|
|
return {
|
2021-11-25 00:45:39 +01:00
|
|
|
|
RequestsMessageKind.REQUEST: (
|
2020-09-28 12:16:57 +02:00
|
|
|
|
OUT_REQ_HEAD in args.output_options,
|
|
|
|
|
OUT_REQ_BODY in args.output_options,
|
|
|
|
|
),
|
2021-11-25 00:45:39 +01:00
|
|
|
|
RequestsMessageKind.RESPONSE: (
|
2020-09-28 12:16:57 +02:00
|
|
|
|
OUT_RESP_HEAD in args.output_options,
|
|
|
|
|
OUT_RESP_BODY in args.output_options,
|
|
|
|
|
),
|
2021-11-25 00:45:39 +01:00
|
|
|
|
}[infer_requests_message_kind(message)]
|
2020-09-25 13:44:28 +02:00
|
|
|
|
|
|
|
|
|
|
2021-01-30 22:14:57 +01:00
|
|
|
|
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
2019-09-03 17:14:39 +02:00
|
|
|
|
"""
|
|
|
|
|
The main program without error handling.
|
|
|
|
|
|
|
|
|
|
"""
|
2021-01-30 22:14:57 +01:00
|
|
|
|
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
|
2019-09-03 17:14:39 +02:00
|
|
|
|
exit_status = ExitStatus.SUCCESS
|
|
|
|
|
downloader = None
|
2021-01-30 22:14:57 +01:00
|
|
|
|
initial_request: Optional[requests.PreparedRequest] = None
|
|
|
|
|
final_response: Optional[requests.Response] = None
|
|
|
|
|
|
|
|
|
|
def separate():
|
|
|
|
|
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
|
|
|
|
|
|
|
|
|
def request_body_read_callback(chunk: bytes):
|
|
|
|
|
should_pipe_to_stdout = bool(
|
|
|
|
|
# Request body output desired
|
|
|
|
|
OUT_REQ_BODY in args.output_options
|
|
|
|
|
# & not `.read()` already pre-request (e.g., for compression)
|
|
|
|
|
and initial_request
|
|
|
|
|
# & non-EOF chunk
|
|
|
|
|
and chunk
|
|
|
|
|
)
|
|
|
|
|
if should_pipe_to_stdout:
|
|
|
|
|
msg = requests.PreparedRequest()
|
|
|
|
|
msg.is_body_upload_chunk = True
|
|
|
|
|
msg.body = chunk
|
|
|
|
|
msg.headers = initial_request.headers
|
|
|
|
|
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
|
2019-09-03 17:14:39 +02:00
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if args.download:
|
|
|
|
|
args.follow = True # --download implies --follow.
|
2021-01-30 22:14:57 +01:00
|
|
|
|
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
2019-09-03 17:14:39 +02:00
|
|
|
|
downloader.pre_request(args.headers)
|
2021-01-30 22:14:57 +01:00
|
|
|
|
messages = collect_messages(args=args, config_dir=env.config.directory,
|
|
|
|
|
request_body_read_callback=request_body_read_callback)
|
|
|
|
|
force_separator = False
|
|
|
|
|
prev_with_body = False
|
2019-09-03 17:14:39 +02:00
|
|
|
|
|
2021-01-30 22:14:57 +01:00
|
|
|
|
# Process messages as they’re generated
|
2020-09-25 13:44:28 +02:00
|
|
|
|
for message in messages:
|
|
|
|
|
is_request = isinstance(message, requests.PreparedRequest)
|
2021-01-30 22:14:57 +01:00
|
|
|
|
with_headers, with_body = get_output_options(args=args, message=message)
|
|
|
|
|
do_write_body = with_body
|
|
|
|
|
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
|
|
|
|
|
# Separate after a previous message with body, if needed. See test_tokens.py.
|
|
|
|
|
separate()
|
|
|
|
|
force_separator = False
|
2020-09-25 13:44:28 +02:00
|
|
|
|
if is_request:
|
2019-09-03 17:14:39 +02:00
|
|
|
|
if not initial_request:
|
|
|
|
|
initial_request = message
|
2021-06-15 13:39:46 +02:00
|
|
|
|
if with_body:
|
2021-01-30 22:14:57 +01:00
|
|
|
|
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
2021-06-15 13:39:46 +02:00
|
|
|
|
do_write_body = not is_streamed_upload
|
|
|
|
|
force_separator = is_streamed_upload and env.stdout_isatty
|
2019-09-03 17:14:39 +02:00
|
|
|
|
else:
|
|
|
|
|
final_response = message
|
|
|
|
|
if args.check_status or downloader:
|
2021-01-30 22:14:57 +01:00
|
|
|
|
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
2021-10-08 14:18:11 +02:00
|
|
|
|
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
2021-01-30 22:14:57 +01:00
|
|
|
|
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,
|
|
|
|
|
with_body=do_write_body)
|
|
|
|
|
prev_with_body = with_body
|
|
|
|
|
|
|
|
|
|
# Cleanup
|
|
|
|
|
if force_separator:
|
|
|
|
|
separate()
|
2019-09-03 17:14:39 +02:00
|
|
|
|
if downloader and exit_status == ExitStatus.SUCCESS:
|
|
|
|
|
# Last response body download.
|
|
|
|
|
download_stream, download_to = downloader.start(
|
|
|
|
|
initial_url=initial_request.url,
|
|
|
|
|
final_response=final_response,
|
|
|
|
|
)
|
2021-01-30 22:14:57 +01:00
|
|
|
|
write_stream(stream=download_stream, outfile=download_to, flush=False)
|
2019-09-03 17:14:39 +02:00
|
|
|
|
downloader.finish()
|
|
|
|
|
if downloader.interrupted:
|
|
|
|
|
exit_status = ExitStatus.ERROR
|
2019-12-02 17:43:16 +01:00
|
|
|
|
env.log_error(
|
2021-05-25 20:49:07 +02:00
|
|
|
|
f'Incomplete download: size={downloader.status.total_size};'
|
|
|
|
|
f' downloaded={downloader.status.downloaded}'
|
|
|
|
|
)
|
2019-09-03 17:14:39 +02:00
|
|
|
|
return exit_status
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
if downloader and not downloader.finished:
|
|
|
|
|
downloader.failed()
|
2021-09-02 16:47:01 +02:00
|
|
|
|
if args.output_file and args.output_file_specified:
|
2019-09-03 17:14:39 +02:00
|
|
|
|
args.output_file.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_debug_info(env: Environment):
|
|
|
|
|
env.stderr.writelines([
|
2019-12-02 17:43:16 +01:00
|
|
|
|
f'HTTPie {httpie_version}\n',
|
|
|
|
|
f'Requests {requests_version}\n',
|
|
|
|
|
f'Pygments {pygments_version}\n',
|
|
|
|
|
f'Python {sys.version}\n{sys.executable}\n',
|
|
|
|
|
f'{platform.system()} {platform.release()}',
|
2019-09-03 17:14:39 +02:00
|
|
|
|
])
|
|
|
|
|
env.stderr.write('\n\n')
|
|
|
|
|
env.stderr.write(repr(env))
|
2021-09-23 17:15:14 +02:00
|
|
|
|
env.stderr.write('\n\n')
|
|
|
|
|
env.stderr.write(repr(plugin_manager))
|
2019-09-03 17:14:39 +02:00
|
|
|
|
env.stderr.write('\n')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def decode_raw_args(
|
|
|
|
|
args: List[Union[str, bytes]],
|
|
|
|
|
stdin_encoding: str
|
|
|
|
|
) -> List[str]:
|
|
|
|
|
"""
|
|
|
|
|
Convert all bytes args to str
|
|
|
|
|
by decoding them using stdin encoding.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
return [
|
|
|
|
|
arg.decode(stdin_encoding)
|
2020-12-21 12:03:25 +01:00
|
|
|
|
if type(arg) is bytes else arg
|
2019-09-03 17:14:39 +02:00
|
|
|
|
for arg in args
|
|
|
|
|
]
|