mirror of
https://github.com/httpie/cli.git
synced 2025-08-17 18:51:08 +02:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
3a3aecca45 | |||
fb3a26586a | |||
cc9083f541 | |||
9ae86f3b4f | |||
3a6fd074a1 | |||
da59381b0b | |||
6de2d6c2cb | |||
b9b033ed0c | |||
64d6363565 | |||
923b7acbe6 | |||
2efc0db8d4 | |||
2bf71af286 | |||
0b84180485 | |||
5a1bd4ba83 | |||
3f7ed35238 | |||
47fd392c74 | |||
54a63a810e | |||
a49774d3ab | |||
b879d38b07 | |||
0913e8b2ef | |||
4fef4b9a75 | |||
bfc23b1412 | |||
6267f21f21 | |||
e9aba543b1 | |||
9b23a4ac9a | |||
b96eba336d | |||
48a6d234cb | |||
c6f2b32e36 | |||
64f6f69037 | |||
6bdfc7a071 | |||
497a91711a | |||
f515ef72d0 | |||
22a2fddc79 | |||
1847eaa299 | |||
e387c1d43e | |||
fc6d89913f | |||
d584686744 | |||
b565be4318 | |||
87e44ae639 | |||
0d08732397 | |||
c53a778f60 | |||
5efc9010cc | |||
08e883fcfe | |||
c4b309164f |
@ -6,9 +6,19 @@ This document records all notable changes to `HTTPie <http://httpie.org>`_.
|
|||||||
This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
||||||
|
|
||||||
|
|
||||||
`1.0.0-dev`_ (Unreleased)
|
`1.0.0-dev`_ (unreleased)
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
`0.9.8`_ (2016-12-08)
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Extended auth plugin API.
|
||||||
|
* Added exit status code ``7`` for plugin errors.
|
||||||
|
* Added support for ``curses``-less Python installations.
|
||||||
|
* Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required.
|
||||||
|
* Improved ``CTRL-C`` interrupt handling.
|
||||||
|
* Added the standard exit status code ``130`` for keyboard interrupts.
|
||||||
|
|
||||||
|
|
||||||
`0.9.6`_ (2016-08-13)
|
`0.9.6`_ (2016-08-13)
|
||||||
---------------------
|
---------------------
|
||||||
@ -310,5 +320,6 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
|||||||
.. _0.9.2: https://github.com/jkbrzt/httpie/compare/0.9.1...0.9.2
|
.. _0.9.2: https://github.com/jkbrzt/httpie/compare/0.9.1...0.9.2
|
||||||
.. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3
|
.. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3
|
||||||
.. _0.9.4: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.4
|
.. _0.9.4: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.4
|
||||||
.. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.6
|
.. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.4...0.9.6
|
||||||
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.4...master
|
.. _0.9.8: https://github.com/jkbrzt/httpie/compare/0.9.6...0.9.8
|
||||||
|
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.8...master
|
||||||
|
637
README.rst
637
README.rst
File diff suppressed because it is too large
Load Diff
47
extras/httpie.rb
Normal file
47
extras/httpie.rb
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
|
||||||
|
# Only useful for testing until it gets accepted by homebrew maintainers.
|
||||||
|
#
|
||||||
|
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||||
|
#
|
||||||
|
class Httpie < Formula
|
||||||
|
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||||
|
homepage "https://httpie.org/"
|
||||||
|
|
||||||
|
url "https://pypi.python.org/packages/10/cf/da63860ef92f9c90a5bd684d5f162067b26ef113b1c4afb9979c2f5c5a7a/httpie-0.9.7.tar.gz"
|
||||||
|
sha256 "6427c198c80b04e84963890261f29f1e3452b2b4b81e87a403bf22996754e6ec"
|
||||||
|
|
||||||
|
head "https://github.com/jkbrzt/httpie.git"
|
||||||
|
|
||||||
|
depends_on :python3
|
||||||
|
|
||||||
|
resource "requests" do
|
||||||
|
url "https://pypi.python.org/packages/d9/03/155b3e67fe35fe5b6f4227a8d9e96a14fda828b18199800d161bcefc1359/requests-2.12.3.tar.gz"
|
||||||
|
sha256 "de5d266953875e9647e37ef7bfe6ef1a46ff8ddfe61b5b3652edf7ea717ee2b2"
|
||||||
|
end
|
||||||
|
|
||||||
|
resource "pygments" do
|
||||||
|
url "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz"
|
||||||
|
sha256 "88e4c8a91b2af5962bfa5ea2447ec6dd357018e86e94c7d14bd8cacbc5b55d81"
|
||||||
|
end
|
||||||
|
|
||||||
|
def install
|
||||||
|
pyver = Language::Python.major_minor_version "python3"
|
||||||
|
ENV.prepend_create_path "PYTHONPATH", libexec/"vendor/lib/python#{pyver}/site-packages"
|
||||||
|
%w[pygments requests].each do |r|
|
||||||
|
resource(r).stage do
|
||||||
|
system "python3", *Language::Python.setup_install_args(libexec/"vendor")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ENV.prepend_create_path "PYTHONPATH", libexec/"lib/python#{pyver}/site-packages"
|
||||||
|
system "python3", *Language::Python.setup_install_args(libexec)
|
||||||
|
|
||||||
|
bin.install Dir["#{libexec}/bin/*"]
|
||||||
|
bin.env_script_all_files(libexec/"bin", :PYTHONPATH => ENV["PYTHONPATH"])
|
||||||
|
end
|
||||||
|
|
||||||
|
test do
|
||||||
|
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb"
|
||||||
|
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||||
|
end
|
||||||
|
end
|
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.9.6'
|
__version__ = '0.9.8'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +11,11 @@ class ExitStatus:
|
|||||||
"""Exit status code constants."""
|
"""Exit status code constants."""
|
||||||
OK = 0
|
OK = 0
|
||||||
ERROR = 1
|
ERROR = 1
|
||||||
|
PLUGIN_ERROR = 7
|
||||||
|
|
||||||
|
# 128+2 SIGINT <http://www.tldp.org/LDP/abs/html/exitcodes.html>
|
||||||
|
ERROR_CTRL_C = 130
|
||||||
|
|
||||||
ERROR_TIMEOUT = 2
|
ERROR_TIMEOUT = 2
|
||||||
ERROR_TOO_MANY_REDIRECTS = 6
|
ERROR_TOO_MANY_REDIRECTS = 6
|
||||||
|
|
||||||
|
@ -3,8 +3,16 @@
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
from .core import main
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
from .core import main
|
||||||
|
sys.exit(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
from . import ExitStatus
|
||||||
|
sys.exit(ExitStatus.ERROR_CTRL_C)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
main()
|
||||||
|
@ -3,24 +3,27 @@
|
|||||||
NOTE: the CLI interface may change before reaching v1.0.
|
NOTE: the CLI interface may change before reaching v1.0.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from textwrap import dedent, wrap
|
|
||||||
# noinspection PyCompatibility
|
# noinspection PyCompatibility
|
||||||
from argparse import (RawDescriptionHelpFormatter, FileType,
|
from argparse import (
|
||||||
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
|
RawDescriptionHelpFormatter, FileType,
|
||||||
|
OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||||
|
)
|
||||||
|
from textwrap import dedent, wrap
|
||||||
|
|
||||||
from httpie import __doc__, __version__
|
from httpie import __doc__, __version__
|
||||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
from httpie.input import (
|
||||||
from httpie.plugins import plugin_manager
|
HTTPieArgumentParser, KeyValueArgType,
|
||||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
SEP_PROXY, SEP_GROUP_ALL_ITEMS,
|
||||||
from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE
|
|
||||||
from httpie.input import (HTTPieArgumentParser,
|
|
||||||
AuthCredentialsArgType, KeyValueArgType,
|
|
||||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS,
|
|
||||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||||
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||||
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
||||||
readable_file_arg, SSL_VERSION_ARG_MAPPING)
|
readable_file_arg, SSL_VERSION_ARG_MAPPING
|
||||||
|
)
|
||||||
|
from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
|
from httpie.plugins import plugin_manager
|
||||||
|
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||||
|
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||||
|
|
||||||
|
|
||||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||||
@ -41,6 +44,7 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
|||||||
text = dedent(text).strip() + '\n\n'
|
text = dedent(text).strip() + '\n\n'
|
||||||
return text.splitlines()
|
return text.splitlines()
|
||||||
|
|
||||||
|
|
||||||
parser = HTTPieArgumentParser(
|
parser = HTTPieArgumentParser(
|
||||||
formatter_class=HTTPieHelpFormatter,
|
formatter_class=HTTPieHelpFormatter,
|
||||||
description='%s <http://httpie.org>' % __doc__.strip(),
|
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||||
@ -102,6 +106,7 @@ positional.add_argument(
|
|||||||
'items',
|
'items',
|
||||||
metavar='REQUEST_ITEM',
|
metavar='REQUEST_ITEM',
|
||||||
nargs=ZERO_OR_MORE,
|
nargs=ZERO_OR_MORE,
|
||||||
|
default=None,
|
||||||
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
||||||
help=r"""
|
help=r"""
|
||||||
Optional key-value pairs to be included in the request. The separator used
|
Optional key-value pairs to be included in the request. The separator used
|
||||||
@ -413,8 +418,8 @@ sessions.add_argument(
|
|||||||
auth = parser.add_argument_group(title='Authentication')
|
auth = parser.add_argument_group(title='Authentication')
|
||||||
auth.add_argument(
|
auth.add_argument(
|
||||||
'--auth', '-a',
|
'--auth', '-a',
|
||||||
|
default=None,
|
||||||
metavar='USER[:PASS]',
|
metavar='USER[:PASS]',
|
||||||
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
|
||||||
help="""
|
help="""
|
||||||
If only the username is provided (-a username), HTTPie will prompt
|
If only the username is provided (-a username), HTTPie will prompt
|
||||||
for the password.
|
for the password.
|
||||||
@ -422,11 +427,22 @@ auth.add_argument(
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _AuthTypeLazyChoices(object):
|
||||||
|
# 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_plugins = plugin_manager.get_auth_plugins()
|
||||||
auth.add_argument(
|
auth.add_argument(
|
||||||
'--auth-type', '-A',
|
'--auth-type', '-A',
|
||||||
choices=[plugin.auth_type for plugin in _auth_plugins],
|
choices=_AuthTypeLazyChoices(),
|
||||||
default=_auth_plugins[0].auth_type,
|
default=None,
|
||||||
help="""
|
help="""
|
||||||
The authentication mechanism to be used. Defaults to "{default}".
|
The authentication mechanism to be used. Defaults to "{default}".
|
||||||
|
|
||||||
|
@ -145,11 +145,6 @@ def get_requests_kwargs(args, base_headers=None):
|
|||||||
headers.update(args.headers)
|
headers.update(args.headers)
|
||||||
headers = finalize_headers(headers)
|
headers = finalize_headers(headers)
|
||||||
|
|
||||||
credentials = None
|
|
||||||
if args.auth:
|
|
||||||
auth_plugin = plugin_manager.get_auth_plugin(args.auth_type)()
|
|
||||||
credentials = auth_plugin.get_auth(args.auth.key, args.auth.value)
|
|
||||||
|
|
||||||
cert = None
|
cert = None
|
||||||
if args.cert:
|
if args.cert:
|
||||||
cert = args.cert
|
cert = args.cert
|
||||||
@ -168,7 +163,7 @@ def get_requests_kwargs(args, base_headers=None):
|
|||||||
}.get(args.verify, args.verify),
|
}.get(args.verify, args.verify),
|
||||||
'cert': cert,
|
'cert': cert,
|
||||||
'timeout': args.timeout,
|
'timeout': args.timeout,
|
||||||
'auth': credentials,
|
'auth': args.auth,
|
||||||
'proxies': dict((p.key, p.value) for p in args.proxy),
|
'proxies': dict((p.key, p.value) for p in args.proxy),
|
||||||
'files': args.files,
|
'files': args.files,
|
||||||
'allow_redirects': args.follow,
|
'allow_redirects': args.follow,
|
||||||
|
@ -80,7 +80,7 @@ class BaseConfigDict(dict):
|
|||||||
class Config(BaseConfigDict):
|
class Config(BaseConfigDict):
|
||||||
|
|
||||||
name = 'config'
|
name = 'config'
|
||||||
helpurl = 'https://github.com/jkbrzt/httpie#config'
|
helpurl = 'https://httpie.org/docs#config'
|
||||||
about = 'HTTPie configuration file'
|
about = 'HTTPie configuration file'
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
|
try:
|
||||||
|
import curses
|
||||||
|
except ImportError:
|
||||||
|
curses = None # Compiled w/o curses
|
||||||
|
|
||||||
from httpie.compat import is_windows
|
from httpie.compat import is_windows
|
||||||
from httpie.config import DEFAULT_CONFIG_DIR, Config
|
from httpie.config import DEFAULT_CONFIG_DIR, Config
|
||||||
@ -28,17 +32,12 @@ class Environment(object):
|
|||||||
stderr_isatty = stderr.isatty()
|
stderr_isatty = stderr.isatty()
|
||||||
colors = 256
|
colors = 256
|
||||||
if not is_windows:
|
if not is_windows:
|
||||||
import curses
|
if curses:
|
||||||
try:
|
try:
|
||||||
curses.setupterm()
|
curses.setupterm()
|
||||||
try:
|
|
||||||
colors = curses.tigetnum('colors')
|
colors = curses.tigetnum('colors')
|
||||||
except TypeError:
|
|
||||||
# pypy3 (2.4.0)
|
|
||||||
colors = curses.tigetnum(b'colors')
|
|
||||||
except curses.error:
|
except curses.error:
|
||||||
pass
|
pass
|
||||||
del curses
|
|
||||||
else:
|
else:
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
import colorama.initialise
|
import colorama.initialise
|
||||||
|
@ -212,7 +212,7 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
|||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR_CTRL_C
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != ExitStatus.OK:
|
if e.code != ExitStatus.OK:
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
@ -230,7 +230,7 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
|||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
raise
|
raise
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR_CTRL_C
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
if e.code != ExitStatus.OK:
|
if e.code != ExitStatus.OK:
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
|
@ -15,6 +15,7 @@ from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
|||||||
|
|
||||||
# TODO: Use MultiDict for headers once added to `requests`.
|
# TODO: Use MultiDict for headers once added to `requests`.
|
||||||
# https://github.com/jkbrzt/httpie/issues/130
|
# https://github.com/jkbrzt/httpie/issues/130
|
||||||
|
from httpie.plugins import plugin_manager
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27
|
from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27
|
||||||
@ -214,22 +215,14 @@ class HTTPieArgumentParser(ArgumentParser):
|
|||||||
self.env.stdout_isatty = False
|
self.env.stdout_isatty = False
|
||||||
|
|
||||||
def _process_auth(self):
|
def _process_auth(self):
|
||||||
"""
|
# TODO: refactor
|
||||||
If only a username provided via --auth, then ask for a password.
|
self.args.auth_plugin = None
|
||||||
Or, take credentials from the URL, if provided.
|
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
|
||||||
|
auth_type_set = self.args.auth_type is not None
|
||||||
"""
|
|
||||||
url = urlsplit(self.args.url)
|
url = urlsplit(self.args.url)
|
||||||
|
|
||||||
if self.args.auth:
|
if self.args.auth is None and not auth_type_set:
|
||||||
if not self.args.auth.has_password():
|
if url.username is not None:
|
||||||
# Stdin already read (if not a tty) so it's save to prompt.
|
|
||||||
if self.args.ignore_stdin:
|
|
||||||
self.error('Unable to prompt for passwords because'
|
|
||||||
' --ignore-stdin is set.')
|
|
||||||
self.args.auth.prompt_password(url.netloc)
|
|
||||||
|
|
||||||
elif url.username is not None:
|
|
||||||
# Handle http://username:password@hostname/
|
# Handle http://username:password@hostname/
|
||||||
username = url.username
|
username = url.username
|
||||||
password = url.password or ''
|
password = url.password or ''
|
||||||
@ -240,6 +233,41 @@ class HTTPieArgumentParser(ArgumentParser):
|
|||||||
orig=SEP_CREDENTIALS.join([username, password])
|
orig=SEP_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,
|
||||||
|
)
|
||||||
|
|
||||||
def _apply_no_options(self, no_options):
|
def _apply_no_options(self, no_options):
|
||||||
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||||
its default value. This allows for un-setting of options, e.g.,
|
its default value. This allows for un-setting of options, e.g.,
|
||||||
@ -578,6 +606,9 @@ class AuthCredentialsArgType(KeyValueArgType):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
|
||||||
|
|
||||||
|
|
||||||
class RequestItemsDict(OrderedDict):
|
class RequestItemsDict(OrderedDict):
|
||||||
"""Multi-value dict for URL parameters and form data."""
|
"""Multi-value dict for URL parameters and form data."""
|
||||||
|
|
||||||
|
@ -17,13 +17,39 @@ class AuthPlugin(BasePlugin):
|
|||||||
|
|
||||||
See <https://github.com/jkbrzt/httpie-ntlm> for an example auth plugin.
|
See <https://github.com/jkbrzt/httpie-ntlm> for an example auth plugin.
|
||||||
|
|
||||||
|
See also `test_auth_plugins.py`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# The value that should be passed to --auth-type
|
# The value that should be passed to --auth-type
|
||||||
# to use this auth plugin. Eg. "my-auth"
|
# to use this auth plugin. Eg. "my-auth"
|
||||||
auth_type = None
|
auth_type = None
|
||||||
|
|
||||||
def get_auth(self, username, password):
|
# Set to `False` to make it possible to invoke this auth
|
||||||
|
# plugin without requiring the user to specify credentials
|
||||||
|
# through `--auth, -a`.
|
||||||
|
auth_require = True
|
||||||
|
|
||||||
|
# By default the `-a` argument is parsed for `username:password`.
|
||||||
|
# Set this to `False` to disable the parsing and error handling.
|
||||||
|
auth_parse = True
|
||||||
|
|
||||||
|
# If both `auth_parse` and `prompt_password` are set to `True`,
|
||||||
|
# and the value of `-a` lacks the password part,
|
||||||
|
# then the user will be prompted to type the password in.
|
||||||
|
prompt_password = True
|
||||||
|
|
||||||
|
# Will be set to the raw value of `-a` (if provided) before
|
||||||
|
# `get_auth()` gets called.
|
||||||
|
raw_auth = None
|
||||||
|
|
||||||
|
def get_auth(self, username=None, password=None):
|
||||||
"""
|
"""
|
||||||
|
If `auth_parse` is set to `True`, then `username`
|
||||||
|
and `password` contain the parsed credentials.
|
||||||
|
|
||||||
|
Use `self.raw_auth` to access the raw value passed through
|
||||||
|
`--auth, -a`.
|
||||||
|
|
||||||
Return a ``requests.auth.AuthBase`` subclass instance.
|
Return a ``requests.auth.AuthBase`` subclass instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -36,6 +36,7 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
|
|||||||
name = 'Basic HTTP auth'
|
name = 'Basic HTTP auth'
|
||||||
auth_type = 'basic'
|
auth_type = 'basic'
|
||||||
|
|
||||||
|
# noinspection PyMethodOverriding
|
||||||
def get_auth(self, username, password):
|
def get_auth(self, username, password):
|
||||||
return HTTPBasicAuth(username, password)
|
return HTTPBasicAuth(username, password)
|
||||||
|
|
||||||
@ -45,5 +46,6 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
|
|||||||
name = 'Digest HTTP auth'
|
name = 'Digest HTTP auth'
|
||||||
auth_type = 'digest'
|
auth_type = 'digest'
|
||||||
|
|
||||||
|
# noinspection PyMethodOverriding
|
||||||
def get_auth(self, username, password):
|
def get_auth(self, username, password):
|
||||||
return requests.auth.HTTPDigestAuth(username, password)
|
return requests.auth.HTTPDigestAuth(username, password)
|
||||||
|
@ -24,6 +24,9 @@ class PluginManager(object):
|
|||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
self._plugins.append(plugin)
|
self._plugins.append(plugin)
|
||||||
|
|
||||||
|
def unregister(self, plugin):
|
||||||
|
self._plugins.remove(plugin)
|
||||||
|
|
||||||
def load_installed_plugins(self):
|
def load_installed_plugins(self):
|
||||||
for entry_point_name in ENTRY_POINT_NAMES:
|
for entry_point_name in ENTRY_POINT_NAMES:
|
||||||
for entry_point in iter_entry_points(entry_point_name):
|
for entry_point in iter_entry_points(entry_point_name):
|
||||||
|
@ -51,11 +51,10 @@ def get_response(requests_session, session_name,
|
|||||||
dump_request(kwargs)
|
dump_request(kwargs)
|
||||||
session.update_headers(kwargs['headers'])
|
session.update_headers(kwargs['headers'])
|
||||||
|
|
||||||
if args.auth:
|
if args.auth_plugin:
|
||||||
session.auth = {
|
session.auth = {
|
||||||
'type': args.auth_type,
|
'type': args.auth_plugin.auth_type,
|
||||||
'username': args.auth.key,
|
'raw_auth': args.auth_plugin.raw_auth,
|
||||||
'password': args.auth.value,
|
|
||||||
}
|
}
|
||||||
elif session.auth:
|
elif session.auth:
|
||||||
kwargs['auth'] = session.auth
|
kwargs['auth'] = session.auth
|
||||||
@ -75,7 +74,7 @@ def get_response(requests_session, session_name,
|
|||||||
|
|
||||||
|
|
||||||
class Session(BaseConfigDict):
|
class Session(BaseConfigDict):
|
||||||
helpurl = 'https://github.com/jkbrzt/httpie#sessions'
|
helpurl = 'https://httpie.org/docs#sessions'
|
||||||
about = 'HTTPie session file'
|
about = 'HTTPie session file'
|
||||||
|
|
||||||
def __init__(self, path, *args, **kwargs):
|
def __init__(self, path, *args, **kwargs):
|
||||||
@ -147,10 +146,31 @@ class Session(BaseConfigDict):
|
|||||||
auth = self.get('auth', None)
|
auth = self.get('auth', None)
|
||||||
if not auth or not auth['type']:
|
if not auth or not auth['type']:
|
||||||
return
|
return
|
||||||
auth_plugin = plugin_manager.get_auth_plugin(auth['type'])()
|
|
||||||
return auth_plugin.get_auth(auth['username'], auth['password'])
|
plugin = plugin_manager.get_auth_plugin(auth['type'])()
|
||||||
|
|
||||||
|
credentials = {'username': None, 'password': None}
|
||||||
|
try:
|
||||||
|
# New style
|
||||||
|
plugin.raw_auth = auth['raw_auth']
|
||||||
|
except KeyError:
|
||||||
|
# Old style
|
||||||
|
credentials = {
|
||||||
|
'username': auth['username'],
|
||||||
|
'password': auth['password'],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
if plugin.auth_parse:
|
||||||
|
from httpie.input import parse_auth
|
||||||
|
parsed = parse_auth(plugin.raw_auth)
|
||||||
|
credentials = {
|
||||||
|
'username': parsed.key,
|
||||||
|
'password': parsed.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.get_auth(**credentials)
|
||||||
|
|
||||||
@auth.setter
|
@auth.setter
|
||||||
def auth(self, auth):
|
def auth(self, auth):
|
||||||
assert set(['type', 'username', 'password']) == set(auth.keys())
|
assert set(['type', 'raw_auth']) == set(auth.keys())
|
||||||
self['auth'] = auth
|
self['auth'] = auth
|
||||||
|
1
setup.py
1
setup.py
@ -69,6 +69,7 @@ def long_description():
|
|||||||
with codecs.open('README.rst', encoding='utf8') as f:
|
with codecs.open('README.rst', encoding='utf8') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='httpie',
|
name='httpie',
|
||||||
version=httpie.__version__,
|
version=httpie.__version__,
|
||||||
|
@ -60,5 +60,16 @@ def test_only_username_in_url(url):
|
|||||||
"""
|
"""
|
||||||
args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
|
args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
|
||||||
assert args.auth
|
assert args.auth
|
||||||
assert args.auth.key == 'username'
|
assert args.auth.username == 'username'
|
||||||
assert args.auth.value == ''
|
assert args.auth.password == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_auth(httpbin):
|
||||||
|
r = http(
|
||||||
|
'--auth-type=basic',
|
||||||
|
'GET',
|
||||||
|
httpbin + '/basic-auth/user/password',
|
||||||
|
error_exit_ok=True
|
||||||
|
)
|
||||||
|
assert HTTP_OK not in r
|
||||||
|
assert '--auth required' in r.stderr
|
||||||
|
133
tests/test_auth_plugins.py
Normal file
133
tests/test_auth_plugins.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
from mock import mock
|
||||||
|
|
||||||
|
from httpie.input import SEP_CREDENTIALS
|
||||||
|
from httpie.plugins import AuthPlugin, plugin_manager
|
||||||
|
from utils import http, HTTP_OK
|
||||||
|
|
||||||
|
# TODO: run all these tests in session mode as well
|
||||||
|
|
||||||
|
USERNAME = 'user'
|
||||||
|
PASSWORD = 'password'
|
||||||
|
# Basic auth encoded `USERNAME` and `PASSWORD`
|
||||||
|
# noinspection SpellCheckingInspection
|
||||||
|
BASIC_AUTH_HEADER_VALUE = 'Basic dXNlcjpwYXNzd29yZA=='
|
||||||
|
BASIC_AUTH_URL = '/basic-auth/{0}/{1}'.format(USERNAME, PASSWORD)
|
||||||
|
AUTH_OK = {'authenticated': True, 'user': USERNAME}
|
||||||
|
|
||||||
|
|
||||||
|
def basic_auth(header=BASIC_AUTH_HEADER_VALUE):
|
||||||
|
|
||||||
|
def inner(r):
|
||||||
|
r.headers['Authorization'] = header
|
||||||
|
return r
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_plugin_parse_auth_false(httpbin):
|
||||||
|
|
||||||
|
class Plugin(AuthPlugin):
|
||||||
|
auth_type = 'test-parse-false'
|
||||||
|
auth_parse = False
|
||||||
|
|
||||||
|
def get_auth(self, username=None, password=None):
|
||||||
|
assert username is None
|
||||||
|
assert password is None
|
||||||
|
assert self.raw_auth == BASIC_AUTH_HEADER_VALUE
|
||||||
|
return basic_auth(self.raw_auth)
|
||||||
|
|
||||||
|
plugin_manager.register(Plugin)
|
||||||
|
try:
|
||||||
|
r = http(
|
||||||
|
httpbin + BASIC_AUTH_URL,
|
||||||
|
'--auth-type',
|
||||||
|
Plugin.auth_type,
|
||||||
|
'--auth',
|
||||||
|
BASIC_AUTH_HEADER_VALUE,
|
||||||
|
)
|
||||||
|
assert HTTP_OK in r
|
||||||
|
assert r.json == AUTH_OK
|
||||||
|
finally:
|
||||||
|
plugin_manager.unregister(Plugin)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_plugin_require_auth_false(httpbin):
|
||||||
|
|
||||||
|
class Plugin(AuthPlugin):
|
||||||
|
auth_type = 'test-require-false'
|
||||||
|
auth_require = False
|
||||||
|
|
||||||
|
def get_auth(self, username=None, password=None):
|
||||||
|
assert self.raw_auth is None
|
||||||
|
assert username is None
|
||||||
|
assert password is None
|
||||||
|
return basic_auth()
|
||||||
|
|
||||||
|
plugin_manager.register(Plugin)
|
||||||
|
try:
|
||||||
|
r = http(
|
||||||
|
httpbin + BASIC_AUTH_URL,
|
||||||
|
'--auth-type',
|
||||||
|
Plugin.auth_type,
|
||||||
|
)
|
||||||
|
assert HTTP_OK in r
|
||||||
|
assert r.json == AUTH_OK
|
||||||
|
finally:
|
||||||
|
plugin_manager.unregister(Plugin)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_plugin_require_auth_false_and_auth_provided(httpbin):
|
||||||
|
|
||||||
|
class Plugin(AuthPlugin):
|
||||||
|
auth_type = 'test-require-false-yet-provided'
|
||||||
|
auth_require = False
|
||||||
|
|
||||||
|
def get_auth(self, username=None, password=None):
|
||||||
|
assert self.raw_auth == USERNAME + SEP_CREDENTIALS + PASSWORD
|
||||||
|
assert username == USERNAME
|
||||||
|
assert password == PASSWORD
|
||||||
|
return basic_auth()
|
||||||
|
|
||||||
|
plugin_manager.register(Plugin)
|
||||||
|
try:
|
||||||
|
r = http(
|
||||||
|
httpbin + BASIC_AUTH_URL,
|
||||||
|
'--auth-type',
|
||||||
|
Plugin.auth_type,
|
||||||
|
'--auth',
|
||||||
|
USERNAME + SEP_CREDENTIALS + PASSWORD,
|
||||||
|
)
|
||||||
|
assert HTTP_OK in r
|
||||||
|
assert r.json == AUTH_OK
|
||||||
|
finally:
|
||||||
|
plugin_manager.unregister(Plugin)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('httpie.input.AuthCredentials._getpass',
|
||||||
|
new=lambda self, prompt: 'UNEXPECTED_PROMPT_RESPONSE')
|
||||||
|
def test_auth_plugin_prompt_password_false(httpbin):
|
||||||
|
|
||||||
|
class Plugin(AuthPlugin):
|
||||||
|
auth_type = 'test-prompt-false'
|
||||||
|
prompt_password = False
|
||||||
|
|
||||||
|
def get_auth(self, username=None, password=None):
|
||||||
|
assert self.raw_auth == USERNAME
|
||||||
|
assert username == USERNAME
|
||||||
|
assert password is None
|
||||||
|
return basic_auth()
|
||||||
|
|
||||||
|
plugin_manager.register(Plugin)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = http(
|
||||||
|
httpbin + BASIC_AUTH_URL,
|
||||||
|
'--auth-type',
|
||||||
|
Plugin.auth_type,
|
||||||
|
'--auth',
|
||||||
|
USERNAME,
|
||||||
|
)
|
||||||
|
assert HTTP_OK in r
|
||||||
|
assert r.json == AUTH_OK
|
||||||
|
finally:
|
||||||
|
plugin_manager.unregister(Plugin)
|
@ -1,8 +1,8 @@
|
|||||||
"""Tests for dealing with binary request and response data."""
|
"""Tests for dealing with binary request and response data."""
|
||||||
|
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
|
||||||
from httpie.compat import urlopen
|
from httpie.compat import urlopen
|
||||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||||
from utils import TestEnvironment, http
|
from utils import TestEnvironment, http
|
||||||
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
|
|
||||||
|
|
||||||
|
|
||||||
class TestBinaryRequestData:
|
class TestBinaryRequestData:
|
||||||
|
@ -1,9 +1,25 @@
|
|||||||
|
import mock
|
||||||
|
|
||||||
from httpie import ExitStatus
|
from httpie import ExitStatus
|
||||||
from utils import TestEnvironment, http, HTTP_OK
|
from utils import TestEnvironment, http, HTTP_OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_keyboard_interrupt_during_arg_parsing_exit_status(httpbin):
|
||||||
|
with mock.patch('httpie.cli.parser.parse_args',
|
||||||
|
side_effect=KeyboardInterrupt()):
|
||||||
|
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
|
||||||
|
assert r.exit_status == ExitStatus.ERROR_CTRL_C
|
||||||
|
|
||||||
|
|
||||||
|
def test_keyboard_interrupt_in_program_exit_status(httpbin):
|
||||||
|
with mock.patch('httpie.core.program',
|
||||||
|
side_effect=KeyboardInterrupt()):
|
||||||
|
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
|
||||||
|
assert r.exit_status == ExitStatus.ERROR_CTRL_C
|
||||||
|
|
||||||
|
|
||||||
def test_ok_response_exits_0(httpbin):
|
def test_ok_response_exits_0(httpbin):
|
||||||
r = http('GET', httpbin.url + '/status/200')
|
r = http('GET', httpbin.url + '/get')
|
||||||
assert HTTP_OK in r
|
assert HTTP_OK in r
|
||||||
assert r.exit_status == ExitStatus.OK
|
assert r.exit_status == ExitStatus.OK
|
||||||
|
|
||||||
|
@ -85,6 +85,15 @@ def test_headers_unset(httpbin_both):
|
|||||||
assert 'Accept' not in r.json['headers'] # default Accept unset
|
assert 'Accept' not in r.json['headers'] # default Accept unset
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip('unimplemented')
|
||||||
|
def test_unset_host_header(httpbin_both):
|
||||||
|
r = http('GET', httpbin_both + '/headers')
|
||||||
|
assert 'Host' in r.json['headers'] # default Host present
|
||||||
|
|
||||||
|
r = http('GET', httpbin_both + '/headers', 'Host:')
|
||||||
|
assert 'Host' not in r.json['headers'] # default Host unset
|
||||||
|
|
||||||
|
|
||||||
def test_headers_empty_value(httpbin_both):
|
def test_headers_empty_value(httpbin_both):
|
||||||
r = http('GET', httpbin_both + '/headers')
|
r = http('GET', httpbin_both + '/headers')
|
||||||
assert r.json['headers']['Accept'] # default Accept has value
|
assert r.json['headers']['Accept'] # default Accept has value
|
||||||
|
@ -192,7 +192,7 @@ def http(*args, **kwargs):
|
|||||||
args_with_config_defaults = args + env.config.default_options
|
args_with_config_defaults = args + env.config.default_options
|
||||||
add_to_args = []
|
add_to_args = []
|
||||||
if '--debug' not in args_with_config_defaults:
|
if '--debug' not in args_with_config_defaults:
|
||||||
if '--traceback' not in args_with_config_defaults:
|
if not error_exit_ok and '--traceback' not in args_with_config_defaults:
|
||||||
add_to_args.append('--traceback')
|
add_to_args.append('--traceback')
|
||||||
if not any('--timeout' in arg for arg in args_with_config_defaults):
|
if not any('--timeout' in arg for arg in args_with_config_defaults):
|
||||||
add_to_args.append('--timeout=3')
|
add_to_args.append('--timeout=3')
|
||||||
|
Reference in New Issue
Block a user