You've already forked httpie-cli
mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 22:42:05 +02:00
Refactoring
This commit is contained in:
0
httpie/cli/__init__.py
Normal file
0
httpie/cli/__init__.py
Normal file
387
httpie/cli/argparser.py
Normal file
387
httpie/cli/argparser.py
Normal file
@@ -0,0 +1,387 @@
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from argparse import RawDescriptionHelpFormatter
|
||||
from textwrap import dedent
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from httpie.cli.argtypes import AuthCredentials, KeyValueArgType, parse_auth
|
||||
from httpie.cli.constants import (
|
||||
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED, OUT_RESP_BODY, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS,
|
||||
SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE,
|
||||
)
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from httpie.cli.requestitems import RequestItems
|
||||
from httpie.context import Environment
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.utils import ExplicitNullAuth, get_content_type
|
||||
|
||||
|
||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
"""A nicer help formatter.
|
||||
|
||||
Help for arguments can be indented and contain new lines.
|
||||
It will be de-dented and arguments in the help
|
||||
will be separated by a blank line for better readability.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, max_help_position=6, *args, **kwargs):
|
||||
# A smaller indent for args help.
|
||||
kwargs['max_help_position'] = max_help_position
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _split_lines(self, text, width):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
|
||||
|
||||
class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||
and performs extra validation.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
kwargs['add_help'] = False
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
self.env = None
|
||||
self.args = None
|
||||
self.has_stdin_data = False
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(
|
||||
self,
|
||||
env: Environment,
|
||||
program_name='http',
|
||||
args=None,
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.args, no_options = super().parse_known_args(args, namespace)
|
||||
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
|
||||
self.has_stdin_data = (
|
||||
self.env.stdin
|
||||
and not self.args.ignore_stdin
|
||||
and not self.env.stdin_isatty
|
||||
)
|
||||
|
||||
# Arguments processing and environment setup.
|
||||
self._apply_no_options(no_options)
|
||||
self._validate_download_options()
|
||||
self._setup_standard_streams()
|
||||
self._process_output_options()
|
||||
self._process_pretty_options()
|
||||
self._guess_method()
|
||||
self._parse_items()
|
||||
|
||||
if self.has_stdin_data:
|
||||
self._body_from_file(self.env.stdin)
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
if os.path.basename(program_name) == 'https':
|
||||
scheme = 'https://'
|
||||
else:
|
||||
scheme = self.args.default_scheme + "://"
|
||||
|
||||
# See if we're using curl style shorthand for localhost (:3000/foo)
|
||||
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
|
||||
if shorthand:
|
||||
port = shorthand.group(1)
|
||||
rest = shorthand.group(2)
|
||||
self.args.url = scheme + 'localhost'
|
||||
if port:
|
||||
self.args.url += ':' + port
|
||||
self.args.url += rest
|
||||
else:
|
||||
self.args.url = scheme + self.args.url
|
||||
self._process_auth()
|
||||
|
||||
return self.args
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _print_message(self, message, file=None):
|
||||
# Sneak in our stderr/stdout.
|
||||
file = {
|
||||
sys.stdout: self.env.stdout,
|
||||
sys.stderr: self.env.stderr,
|
||||
None: self.env.stderr
|
||||
}.get(file, file)
|
||||
if not hasattr(file, 'buffer') and isinstance(message, str):
|
||||
message = message.encode(self.env.stdout_encoding)
|
||||
super()._print_message(message, file)
|
||||
|
||||
def _setup_standard_streams(self):
|
||||
"""
|
||||
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
||||
|
||||
"""
|
||||
self.args.output_file_specified = bool(self.args.output_file)
|
||||
if self.args.download:
|
||||
# FIXME: Come up with a cleaner solution.
|
||||
if not self.args.output_file and not self.env.stdout_isatty:
|
||||
# Use stdout as the download output file.
|
||||
self.args.output_file = self.env.stdout
|
||||
# With `--download`, we write everything that would normally go to
|
||||
# `stdout` to `stderr` instead. Let's replace the stream so that
|
||||
# we don't have to use many `if`s throughout the codebase.
|
||||
# The response body will be treated separately.
|
||||
self.env.stdout = self.env.stderr
|
||||
self.env.stdout_isatty = self.env.stderr_isatty
|
||||
elif self.args.output_file:
|
||||
# When not `--download`ing, then `--output` simply replaces
|
||||
# `stdout`. The file is opened for appending, which isn't what
|
||||
# we want in this case.
|
||||
self.args.output_file.seek(0)
|
||||
try:
|
||||
self.args.output_file.truncate()
|
||||
except IOError as e:
|
||||
if e.errno == errno.EINVAL:
|
||||
# E.g. /dev/null on Linux.
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
self.env.stdout = self.args.output_file
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
def _process_auth(self):
|
||||
# TODO: refactor
|
||||
self.args.auth_plugin = None
|
||||
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
|
||||
auth_type_set = self.args.auth_type is not None
|
||||
url = urlsplit(self.args.url)
|
||||
|
||||
if self.args.auth is None and not auth_type_set:
|
||||
if url.username is not None:
|
||||
# Handle http://username:password@hostname/
|
||||
username = url.username
|
||||
password = url.password or ''
|
||||
self.args.auth = AuthCredentials(
|
||||
key=username,
|
||||
value=password,
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=SEPARATOR_CREDENTIALS.join([username, password])
|
||||
)
|
||||
|
||||
if self.args.auth is not None or auth_type_set:
|
||||
if not self.args.auth_type:
|
||||
self.args.auth_type = default_auth_plugin.auth_type
|
||||
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
|
||||
|
||||
if plugin.auth_require and self.args.auth is None:
|
||||
self.error('--auth required')
|
||||
|
||||
plugin.raw_auth = self.args.auth
|
||||
self.args.auth_plugin = plugin
|
||||
already_parsed = isinstance(self.args.auth, AuthCredentials)
|
||||
|
||||
if self.args.auth is None or not plugin.auth_parse:
|
||||
self.args.auth = plugin.get_auth()
|
||||
else:
|
||||
if already_parsed:
|
||||
# from the URL
|
||||
credentials = self.args.auth
|
||||
else:
|
||||
credentials = parse_auth(self.args.auth)
|
||||
|
||||
if (not credentials.has_password()
|
||||
and plugin.prompt_password):
|
||||
if self.args.ignore_stdin:
|
||||
# Non-tty stdin read by now
|
||||
self.error(
|
||||
'Unable to prompt for passwords because'
|
||||
' --ignore-stdin is set.'
|
||||
)
|
||||
credentials.prompt_password(url.netloc)
|
||||
self.args.auth = plugin.get_auth(
|
||||
username=credentials.key,
|
||||
password=credentials.value,
|
||||
)
|
||||
if not self.args.auth and self.args.ignore_netrc:
|
||||
# Set a no-op auth to force requests to ignore .netrc
|
||||
# <https://github.com/psf/requests/issues/2773#issuecomment-174312831>
|
||||
self.args.auth = ExplicitNullAuth()
|
||||
|
||||
def _apply_no_options(self, no_options):
|
||||
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||
its default value. This allows for un-setting of options, e.g.,
|
||||
specified in config.
|
||||
|
||||
"""
|
||||
invalid = []
|
||||
|
||||
for option in no_options:
|
||||
if not option.startswith('--no-'):
|
||||
invalid.append(option)
|
||||
continue
|
||||
|
||||
# --no-option => --option
|
||||
inverted = '--' + option[5:]
|
||||
for action in self._actions:
|
||||
if inverted in action.option_strings:
|
||||
setattr(self.args, action.dest, action.default)
|
||||
break
|
||||
else:
|
||||
invalid.append(option)
|
||||
|
||||
if invalid:
|
||||
msg = 'unrecognized arguments: %s'
|
||||
self.error(msg % ' '.join(invalid))
|
||||
|
||||
def _body_from_file(self, fd):
|
||||
"""There can only be one source of request data.
|
||||
|
||||
Bytes are always read.
|
||||
|
||||
"""
|
||||
if self.args.data:
|
||||
self.error('Request body (from stdin or a file) and request '
|
||||
'data (key=value) cannot be mixed. Pass '
|
||||
'--ignore-stdin to let key/value take priority.')
|
||||
self.args.data = getattr(fd, 'buffer', fd).read()
|
||||
|
||||
def _guess_method(self):
|
||||
"""Set `args.method` if not specified to either POST or GET
|
||||
based on whether the request has data or not.
|
||||
|
||||
"""
|
||||
if self.args.method is None:
|
||||
# Invoked as `http URL'.
|
||||
assert not self.args.request_items
|
||||
if self.has_stdin_data:
|
||||
self.args.method = HTTP_POST
|
||||
else:
|
||||
self.args.method = HTTP_GET
|
||||
|
||||
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
|
||||
elif not re.match('^[a-zA-Z]+$', self.args.method):
|
||||
# Invoked as `http URL item+'. The URL is now in `args.method`
|
||||
# and the first ITEM is now incorrectly in `args.url`.
|
||||
try:
|
||||
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
||||
self.args.request_items.insert(0, KeyValueArgType(
|
||||
*SEPARATOR_GROUP_ALL_ITEMS).__call__(self.args.url))
|
||||
|
||||
except argparse.ArgumentTypeError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
|
||||
else:
|
||||
# Set the URL correctly
|
||||
self.args.url = self.args.method
|
||||
# Infer the method
|
||||
has_data = (
|
||||
self.has_stdin_data
|
||||
or any(
|
||||
item.sep in SEPARATOR_GROUP_DATA_ITEMS
|
||||
for item in self.args.request_items)
|
||||
)
|
||||
self.args.method = HTTP_POST if has_data else HTTP_GET
|
||||
|
||||
def _parse_items(self):
|
||||
"""
|
||||
Parse `args.request_items` into `args.headers`, `args.data`,
|
||||
`args.params`, and `args.files`.
|
||||
|
||||
"""
|
||||
try:
|
||||
request_items = RequestItems.from_args(
|
||||
request_item_args=self.args.request_items,
|
||||
as_form=self.args.form,
|
||||
)
|
||||
except ParseError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
else:
|
||||
self.args.headers = request_items.headers
|
||||
self.args.data = request_items.data
|
||||
self.args.files = request_items.files
|
||||
self.args.params = request_items.params
|
||||
|
||||
if self.args.files and not self.args.form:
|
||||
# `http url @/path/to/file`
|
||||
file_fields = list(self.args.files.keys())
|
||||
if file_fields != ['']:
|
||||
self.error(
|
||||
'Invalid file fields (perhaps you meant --form?): %s'
|
||||
% ','.join(file_fields))
|
||||
|
||||
fn, fd, ct = self.args.files['']
|
||||
self.args.files = {}
|
||||
|
||||
self._body_from_file(fd)
|
||||
|
||||
if 'Content-Type' not in self.args.headers:
|
||||
content_type = get_content_type(fn)
|
||||
if content_type:
|
||||
self.args.headers['Content-Type'] = content_type
|
||||
|
||||
def _process_output_options(self):
|
||||
"""Apply defaults to output options, or validate the provided ones.
|
||||
|
||||
The default output options are stdout-type-sensitive.
|
||||
|
||||
"""
|
||||
|
||||
def check_options(value, option):
|
||||
unknown = set(value) - OUTPUT_OPTIONS
|
||||
if unknown:
|
||||
self.error('Unknown output options: {0}={1}'.format(
|
||||
option,
|
||||
','.join(unknown)
|
||||
))
|
||||
|
||||
if self.args.verbose:
|
||||
self.args.all = True
|
||||
|
||||
if self.args.output_options is None:
|
||||
if self.args.verbose:
|
||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||
else:
|
||||
self.args.output_options = (
|
||||
OUTPUT_OPTIONS_DEFAULT
|
||||
if self.env.stdout_isatty
|
||||
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
|
||||
)
|
||||
|
||||
if self.args.output_options_history is None:
|
||||
self.args.output_options_history = self.args.output_options
|
||||
|
||||
check_options(self.args.output_options, '--print')
|
||||
check_options(self.args.output_options_history, '--history-print')
|
||||
|
||||
if self.args.download and OUT_RESP_BODY in self.args.output_options:
|
||||
# Response body is always downloaded with --download and it goes
|
||||
# through a different routine, so we remove it.
|
||||
self.args.output_options = str(
|
||||
set(self.args.output_options) - set(OUT_RESP_BODY))
|
||||
|
||||
def _process_pretty_options(self):
|
||||
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
|
||||
self.args.prettify = PRETTY_MAP[
|
||||
'all' if self.env.stdout_isatty else 'none']
|
||||
elif (self.args.prettify and self.env.is_windows
|
||||
and self.args.output_file):
|
||||
self.error('Only terminal output can be colorized on Windows.')
|
||||
else:
|
||||
# noinspection PyTypeChecker
|
||||
self.args.prettify = PRETTY_MAP[self.args.prettify]
|
||||
|
||||
def _validate_download_options(self):
|
||||
if not self.args.download:
|
||||
if self.args.download_resume:
|
||||
self.error('--continue only works with --download')
|
||||
if self.args.download_resume and not (
|
||||
self.args.download and self.args.output_file):
|
||||
self.error('--continue requires --output to be specified')
|
180
httpie/cli/argtypes.py
Normal file
180
httpie/cli/argtypes.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
|
||||
from httpie.cli.constants import SEPARATOR_CREDENTIALS
|
||||
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
class KeyValueArg:
|
||||
"""Base key-value pair parsed from CLI."""
|
||||
|
||||
def __init__(self, key, value, sep, orig):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.sep = sep
|
||||
self.orig = orig
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
|
||||
class SessionNameValidator:
|
||||
|
||||
def __init__(self, error_message):
|
||||
self.error_message = error_message
|
||||
|
||||
def __call__(self, value):
|
||||
# Session name can be a path or just a name.
|
||||
if (os.path.sep not in value
|
||||
and not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||
raise argparse.ArgumentError(None, self.error_message)
|
||||
return value
|
||||
|
||||
|
||||
class Escaped(str):
|
||||
"""Represents an escaped character."""
|
||||
|
||||
|
||||
class KeyValueArgType:
|
||||
"""A key-value pair argument type used with `argparse`.
|
||||
|
||||
Parses a key-value arg and constructs a `KeyValuArge` instance.
|
||||
Used for headers, form data, and other key-value pair types.
|
||||
|
||||
"""
|
||||
|
||||
key_value_class = KeyValueArg
|
||||
|
||||
def __init__(self, *separators):
|
||||
self.separators = separators
|
||||
self.special_characters = set('\\')
|
||||
for separator in separators:
|
||||
self.special_characters.update(separator)
|
||||
|
||||
def __call__(self, string) -> KeyValueArg:
|
||||
"""Parse `string` and return `self.key_value_class()` instance.
|
||||
|
||||
The best of `self.separators` is determined (first found, longest).
|
||||
Back slash escaped characters aren't considered as separators
|
||||
(or parts thereof). Literal back slash characters have to be escaped
|
||||
as well (r'\\').
|
||||
|
||||
"""
|
||||
|
||||
def tokenize(string):
|
||||
r"""Tokenize `string`. There are only two token types - strings
|
||||
and escaped characters:
|
||||
|
||||
tokenize(r'foo\=bar\\baz')
|
||||
=> ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
|
||||
|
||||
"""
|
||||
tokens = ['']
|
||||
characters = iter(string)
|
||||
for char in characters:
|
||||
if char == '\\':
|
||||
char = next(characters, '')
|
||||
if char not in self.special_characters:
|
||||
tokens[-1] += '\\' + char
|
||||
else:
|
||||
tokens.extend([Escaped(char), ''])
|
||||
else:
|
||||
tokens[-1] += char
|
||||
return tokens
|
||||
|
||||
tokens = tokenize(string)
|
||||
|
||||
# Sorting by length ensures that the longest one will be
|
||||
# chosen as it will overwrite any shorter ones starting
|
||||
# at the same position in the `found` dictionary.
|
||||
separators = sorted(self.separators, key=len)
|
||||
|
||||
for i, token in enumerate(tokens):
|
||||
|
||||
if isinstance(token, Escaped):
|
||||
continue
|
||||
|
||||
found = {}
|
||||
for sep in separators:
|
||||
pos = token.find(sep)
|
||||
if pos != -1:
|
||||
found[pos] = sep
|
||||
|
||||
if found:
|
||||
# Starting first, longest separator found.
|
||||
sep = found[min(found.keys())]
|
||||
|
||||
key, value = token.split(sep, 1)
|
||||
|
||||
# Any preceding tokens are part of the key.
|
||||
key = ''.join(tokens[:i]) + key
|
||||
|
||||
# Any following tokens are part of the value.
|
||||
value += ''.join(tokens[i + 1:])
|
||||
|
||||
break
|
||||
|
||||
else:
|
||||
raise argparse.ArgumentTypeError(
|
||||
u'"%s" is not a valid value' % string)
|
||||
|
||||
return self.key_value_class(
|
||||
key=key, value=value, sep=sep, orig=string)
|
||||
|
||||
|
||||
class AuthCredentials(KeyValueArg):
|
||||
"""Represents parsed credentials."""
|
||||
|
||||
def _getpass(self, prompt):
|
||||
# To allow mocking.
|
||||
return getpass.getpass(str(prompt))
|
||||
|
||||
def has_password(self):
|
||||
return self.value is not None
|
||||
|
||||
def prompt_password(self, host):
|
||||
try:
|
||||
self.value = self._getpass(
|
||||
'http: password for %s@%s: ' % (self.key, host))
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class AuthCredentialsArgType(KeyValueArgType):
|
||||
"""A key-value arg type that parses credentials."""
|
||||
|
||||
key_value_class = AuthCredentials
|
||||
|
||||
def __call__(self, string):
|
||||
"""Parse credentials from `string`.
|
||||
|
||||
("username" or "username:password").
|
||||
|
||||
"""
|
||||
try:
|
||||
return super().__call__(string)
|
||||
except argparse.ArgumentTypeError:
|
||||
# No password provided, will prompt for it later.
|
||||
return self.key_value_class(
|
||||
key=string,
|
||||
value=None,
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=string
|
||||
)
|
||||
|
||||
|
||||
parse_auth = AuthCredentialsArgType(SEPARATOR_CREDENTIALS)
|
||||
|
||||
|
||||
def readable_file_arg(filename):
|
||||
try:
|
||||
with open(filename, 'rb'):
|
||||
return filename
|
||||
except IOError as ex:
|
||||
raise argparse.ArgumentTypeError('%s: %s' % (filename, ex.args[1]))
|
102
httpie/cli/constants.py
Normal file
102
httpie/cli/constants.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Parsing and processing of CLI input (args, auth credentials, files, stdin).
|
||||
|
||||
"""
|
||||
import re
|
||||
import ssl
|
||||
|
||||
|
||||
# TODO: Use MultiDict for headers once added to `requests`.
|
||||
# https://github.com/jakubroztocil/httpie/issues/130
|
||||
|
||||
|
||||
# ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
# <https://tools.ietf.org/html/rfc3986#section-3.1>
|
||||
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
|
||||
# Various separators used in args
|
||||
SEPARATOR_HEADER = ':'
|
||||
SEPARATOR_HEADER_EMPTY = ';'
|
||||
SEPARATOR_CREDENTIALS = ':'
|
||||
SEPARATOR_PROXY = ':'
|
||||
SEPARATOR_DATA_STRING = '='
|
||||
SEPARATOR_DATA_RAW_JSON = ':='
|
||||
SEPARATOR_FILE_UPLOAD = '@'
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEPARATOR_QUERY_PARAM = '=='
|
||||
|
||||
# Separators that become request data
|
||||
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE
|
||||
})
|
||||
|
||||
# Separators for items whose value is a filename to be embedded
|
||||
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
})
|
||||
|
||||
# Separators for raw JSON items
|
||||
SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
# Separators allowed in ITEM arguments
|
||||
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
|
||||
SEPARATOR_HEADER,
|
||||
SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
})
|
||||
|
||||
# Output options
|
||||
OUT_REQ_HEAD = 'H'
|
||||
OUT_REQ_BODY = 'B'
|
||||
OUT_RESP_HEAD = 'h'
|
||||
OUT_RESP_BODY = 'b'
|
||||
|
||||
OUTPUT_OPTIONS = frozenset({
|
||||
OUT_REQ_HEAD,
|
||||
OUT_REQ_BODY,
|
||||
OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY
|
||||
})
|
||||
|
||||
# Pretty
|
||||
PRETTY_MAP = {
|
||||
'all': ['format', 'colors'],
|
||||
'colors': ['colors'],
|
||||
'format': ['format'],
|
||||
'none': []
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||||
'ssl3': 'PROTOCOL_SSLv3',
|
||||
'tls1': 'PROTOCOL_TLSv1',
|
||||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||||
}
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
cli_arg: getattr(ssl, ssl_constant)
|
||||
for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
|
||||
if hasattr(ssl, ssl_constant)
|
||||
}
|
657
httpie/cli/definition.py
Normal file
657
httpie/cli/definition.py
Normal file
@@ -0,0 +1,657 @@
|
||||
"""
|
||||
CLI arguments definition.
|
||||
|
||||
"""
|
||||
from argparse import (FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE)
|
||||
from textwrap import dedent, wrap
|
||||
|
||||
from httpie import __doc__, __version__
|
||||
from httpie.cli.argparser import HTTPieArgumentParser
|
||||
from httpie.cli.argtypes import (
|
||||
KeyValueArgType, SessionNameValidator, readable_file_arg,
|
||||
)
|
||||
from httpie.cli.constants import (
|
||||
OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, SSL_VERSION_ARG_MAPPING,
|
||||
)
|
||||
from httpie.output.formatters.colors import (
|
||||
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
|
||||
)
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
prog='http',
|
||||
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||
epilog=dedent("""
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
|
||||
https://github.com/jakubroztocil/httpie/issues
|
||||
|
||||
"""),
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Positional arguments.
|
||||
#######################################################################
|
||||
|
||||
positional = parser.add_argument_group(
|
||||
title='Positional Arguments',
|
||||
description=dedent("""
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
|
||||
""")
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='method',
|
||||
metavar='METHOD',
|
||||
nargs=OPTIONAL,
|
||||
default=None,
|
||||
help="""
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
This argument can be omitted in which case HTTPie will use POST if there
|
||||
is some data to be sent, otherwise GET:
|
||||
|
||||
$ http example.org # => GET
|
||||
$ http example.org hello=world # => POST
|
||||
|
||||
"""
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='url',
|
||||
metavar='URL',
|
||||
help="""
|
||||
The scheme defaults to 'http://' if the URL does not include one.
|
||||
(You can override this with: --default-scheme=https)
|
||||
|
||||
You can also use a shorthand for localhost
|
||||
|
||||
$ http :3000 # => http://localhost:3000
|
||||
$ http :/foo # => http://localhost/foo
|
||||
|
||||
"""
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='request_items',
|
||||
metavar='REQUEST_ITEM',
|
||||
nargs=ZERO_OR_MORE,
|
||||
default=None,
|
||||
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
||||
help=r"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
':' HTTP headers:
|
||||
|
||||
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
'==' URL parameters to be appended to the request URI:
|
||||
|
||||
search==httpie
|
||||
|
||||
'=' Data fields to be serialized into a JSON object (with --json, -j)
|
||||
or form data (with --form, -f):
|
||||
|
||||
name=HTTPie language=Python description='CLI HTTP client'
|
||||
|
||||
':=' Non-string JSON data fields (only with --json, -j):
|
||||
|
||||
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
|
||||
|
||||
'@' Form file fields (only with --form, -f):
|
||||
|
||||
cs@~/Documents/CV.pdf
|
||||
|
||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
|
||||
|
||||
package:=@./package.json
|
||||
|
||||
You can use a backslash to escape a colliding separator in the field name:
|
||||
|
||||
field-name-with\:colon=value
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Content type.
|
||||
#######################################################################
|
||||
|
||||
content_type = parser.add_argument_group(
|
||||
title='Predefined Content Types',
|
||||
description=None
|
||||
)
|
||||
|
||||
content_type.add_argument(
|
||||
'--json', '-j',
|
||||
action='store_true',
|
||||
help="""
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
(if not specified).
|
||||
|
||||
"""
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--form', '-f',
|
||||
action='store_true',
|
||||
help="""
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||
specified). The presence of any file fields results in a
|
||||
multipart/form-data request.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Content processing.
|
||||
#######################################################################
|
||||
|
||||
content_processing = parser.add_argument_group(
|
||||
title='Content Processing Options',
|
||||
description=None
|
||||
)
|
||||
|
||||
content_processing.add_argument(
|
||||
'--compress', '-x',
|
||||
action='count',
|
||||
help="""
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
|
||||
Compression is skipped if it appears that compression ratio is
|
||||
negative. Compression can be forced by repeating the argument.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output processing
|
||||
#######################################################################
|
||||
|
||||
output_processing = parser.add_argument_group(title='Output Processing')
|
||||
|
||||
output_processing.add_argument(
|
||||
'--pretty',
|
||||
dest='prettify',
|
||||
default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
help="""
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
and formatting (default for terminal output), "colors", or "format".
|
||||
|
||||
"""
|
||||
)
|
||||
output_processing.add_argument(
|
||||
'--style', '-s',
|
||||
dest='style',
|
||||
metavar='STYLE',
|
||||
default=DEFAULT_STYLE,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help="""
|
||||
Output coloring style (default is "{default}"). One of:
|
||||
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
|
||||
""".format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output options
|
||||
#######################################################################
|
||||
output_options = parser.add_argument_group(title='Output Options')
|
||||
|
||||
output_options.add_argument(
|
||||
'--print', '-p',
|
||||
dest='output_options',
|
||||
metavar='WHAT',
|
||||
help=f"""
|
||||
String specifying what the output should contain:
|
||||
|
||||
'{OUT_REQ_HEAD}' request headers
|
||||
'{OUT_REQ_BODY}' request body
|
||||
'{OUT_RESP_HEAD}' response headers
|
||||
'{OUT_RESP_BODY}' response body
|
||||
|
||||
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
If the output is piped to another program or to a file, then only the
|
||||
response body is printed by default.
|
||||
|
||||
"""
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--headers', '-h',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_HEAD,
|
||||
help=f"""
|
||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||
|
||||
"""
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_BODY,
|
||||
help=f"""
|
||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v',
|
||||
dest='verbose',
|
||||
action='store_true',
|
||||
help="""
|
||||
Verbose output. Print the whole request as well as the response. Also print
|
||||
any intermediary requests/responses (such as redirects).
|
||||
It's a shortcut for: --all --print={0}
|
||||
|
||||
""".format(''.join(OUTPUT_OPTIONS))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
followed redirects (with --follow), the first unauthorized request when
|
||||
Digest auth is used (--auth=digest), etc.
|
||||
|
||||
"""
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--history-print', '-P',
|
||||
dest='output_options_history',
|
||||
metavar='WHAT',
|
||||
help="""
|
||||
The same as --print, -p but applies only to intermediary requests/responses
|
||||
(such as redirects) when their inclusion is enabled with --all. If this
|
||||
options is not specified, then they are formatted the same way as the final
|
||||
response.
|
||||
|
||||
"""
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--stream', '-S',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Always stream the output by line, i.e., behave like `tail -f'.
|
||||
|
||||
Without --stream and with --pretty (either set or implied),
|
||||
HTTPie fetches the whole response before it outputs the processed data.
|
||||
|
||||
Set this option when you want to continuously display a prettified
|
||||
long-lived response, such as one from the Twitter streaming API.
|
||||
|
||||
It is useful also without --pretty: It ensures that the output is flushed
|
||||
more often and in smaller chunks.
|
||||
|
||||
"""
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--output', '-o',
|
||||
type=FileType('a+b'),
|
||||
dest='output_file',
|
||||
metavar='FILE',
|
||||
help="""
|
||||
Save output to FILE instead of stdout. If --download is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
"""
|
||||
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--download', '-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with --output
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--continue', '-c',
|
||||
dest='download_resume',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Resume an interrupted download. Note that the --output option needs to be
|
||||
specified as well.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Sessions
|
||||
#######################################################################
|
||||
|
||||
sessions = parser.add_argument_group(title='Sessions') \
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
|
||||
session_name_validator = SessionNameValidator(
|
||||
'Session name contains invalid characters.'
|
||||
)
|
||||
|
||||
sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=f"""
|
||||
Create, or reuse and update a session. Within a session, custom headers,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
Session files are stored in:
|
||||
|
||||
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
"""
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help="""
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Authentication
|
||||
#######################################################################
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
auth = parser.add_argument_group(title='Authentication')
|
||||
auth.add_argument(
|
||||
'--auth', '-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS]',
|
||||
help="""
|
||||
If only the username is provided (-a username), HTTPie will prompt
|
||||
for the password.
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
class _AuthTypeLazyChoices:
|
||||
# Needed for plugin testing
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in plugin_manager.get_auth_plugin_mapping()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys()))
|
||||
|
||||
|
||||
_auth_plugins = plugin_manager.get_auth_plugins()
|
||||
auth.add_argument(
|
||||
'--auth-type', '-A',
|
||||
choices=_AuthTypeLazyChoices(),
|
||||
default=None,
|
||||
help="""
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
|
||||
""".format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
'' if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else ' (provided by %s)' % plugin.package_name
|
||||
),
|
||||
description=(
|
||||
'' if not plugin.description else
|
||||
'\n ' + ('\n '.join(wrap(plugin.description)))
|
||||
)
|
||||
)
|
||||
for plugin in _auth_plugins
|
||||
)),
|
||||
)
|
||||
auth.add_argument(
|
||||
'--ignore-netrc',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Network
|
||||
#######################################################################
|
||||
|
||||
network = parser.add_argument_group(title='Network')
|
||||
|
||||
network.add_argument(
|
||||
'--proxy',
|
||||
default=[],
|
||||
action='append',
|
||||
metavar='PROTOCOL:PROXY_URL',
|
||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||
help="""
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||
and $HTTPS_proxy are supported as well.
|
||||
|
||||
"""
|
||||
)
|
||||
network.add_argument(
|
||||
'--follow', '-F',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Follow 30x Location redirects.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-redirects',
|
||||
type=int,
|
||||
default=30,
|
||||
help="""
|
||||
By default, requests have a limit of 30 redirects (works with --follow).
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-headers',
|
||||
type=int,
|
||||
default=0,
|
||||
help="""
|
||||
The maximum number of response headers to be read before giving up
|
||||
(default 0, i.e., no limit).
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--timeout',
|
||||
type=float,
|
||||
default=0,
|
||||
metavar='SECONDS',
|
||||
help="""
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
This is not a time limit on the entire response download;
|
||||
rather, an error is reported if the server has not issued a response for
|
||||
timeout seconds (more precisely, if no bytes have been received on
|
||||
the underlying socket for timeout seconds).
|
||||
|
||||
"""
|
||||
)
|
||||
network.add_argument(
|
||||
'--check-status',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
|
||||
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
|
||||
status code, HTTPie exits with 4 or 5 respectively. If the response is a
|
||||
3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# SSL
|
||||
#######################################################################
|
||||
|
||||
ssl = parser.add_argument_group(title='SSL')
|
||||
ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
help="""
|
||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||
variable instead.)
|
||||
"""
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
|
||||
dest='ssl_version',
|
||||
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
|
||||
help="""
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. Available protocols
|
||||
may vary depending on OpenSSL installation (only the supported ones
|
||||
are shown here).
|
||||
|
||||
"""
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--cert',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
specify --cert-key separately.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
ssl.add_argument(
|
||||
'--cert-key',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
The private key to use with SSL. Only needed if --cert is given and the
|
||||
certificate file does not contain the private key.
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Troubleshooting
|
||||
#######################################################################
|
||||
|
||||
troubleshooting = parser.add_argument_group(title='Troubleshooting')
|
||||
|
||||
troubleshooting.add_argument(
|
||||
'--ignore-stdin', '-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Do not attempt to read stdin.
|
||||
|
||||
"""
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help',
|
||||
default=SUPPRESS,
|
||||
help="""
|
||||
Show this help message and exit.
|
||||
|
||||
"""
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help="""
|
||||
Show version and exit.
|
||||
|
||||
"""
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
"""
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--default-scheme',
|
||||
default="http",
|
||||
help="""
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
"""
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
"""
|
||||
)
|
53
httpie/cli/dicts.py
Normal file
53
httpie/cli/dicts.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
|
||||
class RequestHeadersDict(CaseInsensitiveDict):
|
||||
"""
|
||||
Headers are case-insensitive and multiple values are currently not supported.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class RequestJSONDataDict(OrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class MultiValueOrderedDict(OrderedDict):
|
||||
"""Multi-value dict for URL parameters and form data."""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
If `key` is assigned more than once, `self[key]` holds a
|
||||
`list` of all the values.
|
||||
|
||||
This allows having multiple fields with the same name in form
|
||||
data and URL params.
|
||||
|
||||
"""
|
||||
assert not isinstance(value, list)
|
||||
if key not in self:
|
||||
super().__setitem__(key, value)
|
||||
else:
|
||||
if not isinstance(self[key], list):
|
||||
super().__setitem__(key, [self[key]])
|
||||
self[key].append(value)
|
||||
|
||||
|
||||
class RequestQueryParamsDict(MultiValueOrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class RequestDataDict(MultiValueOrderedDict):
|
||||
|
||||
def items(self):
|
||||
for key, values in super(MultiValueOrderedDict, self).items():
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
class RequestFilesDict(RequestDataDict):
|
||||
pass
|
2
httpie/cli/exceptions.py
Normal file
2
httpie/cli/exceptions.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class ParseError(Exception):
|
||||
pass
|
162
httpie/cli/requestitems.py
Normal file
162
httpie/cli/requestitems.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
|
||||
|
||||
from httpie.cli.argtypes import KeyValueArg
|
||||
from httpie.cli.constants import (
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD, SEPARATOR_HEADER,
|
||||
SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
)
|
||||
from httpie.cli.dicts import (
|
||||
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict,
|
||||
RequestQueryParamsDict,
|
||||
)
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from httpie.utils import (get_content_type, load_json_preserve_order)
|
||||
|
||||
|
||||
class RequestItems:
|
||||
|
||||
def __init__(self, as_form=False, chunked=False):
|
||||
self.headers = RequestHeadersDict()
|
||||
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
|
||||
self.files = RequestFilesDict()
|
||||
self.params = RequestQueryParamsDict()
|
||||
self.chunked = chunked
|
||||
|
||||
@classmethod
|
||||
def from_args(
|
||||
cls,
|
||||
request_item_args: List[KeyValueArg],
|
||||
as_form=False,
|
||||
chunked=False
|
||||
) -> 'RequestItems':
|
||||
instance = RequestItems(as_form=as_form, chunked=chunked)
|
||||
rules: Dict[str, Tuple[Callable, dict]] = {
|
||||
SEPARATOR_HEADER: (
|
||||
process_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_HEADER_EMPTY: (
|
||||
process_empty_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_QUERY_PARAM: (
|
||||
process_query_param_arg,
|
||||
instance.params,
|
||||
),
|
||||
SEPARATOR_FILE_UPLOAD: (
|
||||
process_file_upload_arg,
|
||||
instance.files,
|
||||
),
|
||||
SEPARATOR_DATA_STRING: (
|
||||
process_data_item_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS: (
|
||||
process_data_embed_file_contents_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_RAW_JSON: (
|
||||
process_data_raw_json_embed_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
|
||||
process_data_embed_raw_json_file_arg,
|
||||
instance.data,
|
||||
),
|
||||
}
|
||||
|
||||
for arg in request_item_args:
|
||||
processor_func, target_dict = rules[arg.sep]
|
||||
target_dict[arg.key] = processor_func(arg)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
JSONType = Union[str, bool, int, list, dict]
|
||||
|
||||
|
||||
def process_header_arg(arg: KeyValueArg) -> Optional[str]:
|
||||
return arg.value or None
|
||||
|
||||
|
||||
def process_empty_header_arg(arg: KeyValueArg) -> str:
|
||||
if arg.value:
|
||||
raise ParseError(
|
||||
'Invalid item "%s" '
|
||||
'(to specify an empty header use `Header;`)'
|
||||
% arg.orig
|
||||
)
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_query_param_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
filename = arg.value
|
||||
try:
|
||||
with open(os.path.expanduser(filename), 'rb') as f:
|
||||
contents = f.read()
|
||||
except IOError as e:
|
||||
raise ParseError('"%s": %s' % (arg.orig, e))
|
||||
return (
|
||||
os.path.basename(filename),
|
||||
BytesIO(contents),
|
||||
get_content_type(filename),
|
||||
)
|
||||
|
||||
|
||||
def parse_file_item_chunked(arg: KeyValueArg):
|
||||
fn = arg.value
|
||||
try:
|
||||
f = open(os.path.expanduser(fn), 'rb')
|
||||
except IOError as e:
|
||||
raise ParseError('"%s": %s' % (arg.orig, e))
|
||||
return os.path.basename(fn), f, get_content_type(fn)
|
||||
|
||||
|
||||
def process_data_item_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg)
|
||||
|
||||
|
||||
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
|
||||
contents = load_text_file(arg)
|
||||
value = load_json(arg, contents)
|
||||
return value
|
||||
|
||||
|
||||
def process_data_raw_json_embed_arg(arg: KeyValueArg) -> JSONType:
|
||||
value = load_json(arg, arg.value)
|
||||
return value
|
||||
|
||||
|
||||
def load_text_file(item) -> str:
|
||||
path = item.value
|
||||
try:
|
||||
with open(os.path.expanduser(path), 'rb') as f:
|
||||
return f.read().decode('utf8')
|
||||
except IOError as e:
|
||||
raise ParseError('"%s": %s' % (item.orig, e))
|
||||
except UnicodeDecodeError:
|
||||
raise ParseError(
|
||||
'"%s": cannot embed the content of "%s",'
|
||||
' not a UTF8 or ASCII-encoded text file'
|
||||
% (item.orig, item.value)
|
||||
)
|
||||
|
||||
|
||||
def load_json(arg: KeyValueArg, contents: str) -> JSONType:
|
||||
try:
|
||||
return load_json_preserve_order(contents)
|
||||
except ValueError as e:
|
||||
raise ParseError('"%s": %s' % (arg.orig, e))
|
Reference in New Issue
Block a user