mirror of
https://github.com/httpie/cli.git
synced 2025-08-16 13:41:43 +02:00
Compare commits
80 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 | |||
8e96238323 | |||
8a9206eceb | |||
8ac3c5961c | |||
487c7a9221 | |||
6d65668355 | |||
3e5115e4a2 | |||
2b8b572f22 | |||
af737fd338 | |||
ee375b6942 | |||
becb63de9a | |||
86c8abc485 | |||
8f6bee9196 | |||
9c2c058ae5 | |||
6238b59e72 | |||
702c21aa91 | |||
aab5cd9da0 | |||
8c0f0b578c | |||
bb4881a873 | |||
3a1726b4ed | |||
e1fa57d228 | |||
bfc64bce21 | |||
595dc51b2d | |||
83fa772247 | |||
49a0fb6e0f | |||
41e822ca2f | |||
1124d68946 | |||
c3735d0422 | |||
364b91cbc4 | |||
c8e06b55e1 | |||
5acbc904b7 | |||
0c7c248dce | |||
caf60cbc65 | |||
2b0e642842 | |||
e25948f6a0 | |||
ec245a1e80 | |||
6259b5dd3b |
19
.travis.yml
19
.travis.yml
@ -15,7 +15,8 @@ python:
|
||||
- pypy
|
||||
- 3.4
|
||||
- 3.5
|
||||
- pypy3
|
||||
# Currently fails because of a Flask issue
|
||||
# - pypy3
|
||||
|
||||
matrix:
|
||||
|
||||
@ -44,6 +45,11 @@ matrix:
|
||||
- TOXENV=py35
|
||||
- BREW_INSTALL=python3
|
||||
|
||||
# Python Codestyle
|
||||
- os: linux
|
||||
python: 3.5
|
||||
env: CODESTYLE=true
|
||||
|
||||
install:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
|
||||
@ -53,11 +59,20 @@ install:
|
||||
fi
|
||||
sudo pip install tox
|
||||
fi
|
||||
if [[ $CODESTYLE ]]; then
|
||||
pip install pycodestyle
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
make
|
||||
if [[ $CODESTYLE ]]; then
|
||||
# 241 - multiple spaces after ‘,’
|
||||
# 501 - line too long
|
||||
pycodestyle --ignore=E241,E501
|
||||
else
|
||||
make
|
||||
fi
|
||||
else
|
||||
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
|
||||
fi
|
||||
|
@ -8,7 +8,7 @@ HTTPie authors
|
||||
Patches and ideas
|
||||
-----------------
|
||||
|
||||
`Complete list of contributors on GitHib <https://github.com/jkbrzt/httpie/graphs/contributors>`_
|
||||
`Complete list of contributors on GitHub <https://github.com/jkbrzt/httpie/graphs/contributors>`_
|
||||
|
||||
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
|
||||
* `Hank Gay <https://github.com/gthank>`_
|
||||
|
@ -6,9 +6,40 @@ This document records all notable changes to `HTTPie <http://httpie.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)
|
||||
---------------------
|
||||
|
||||
* Added Python 3 as a dependency for Homebrew installations
|
||||
to ensure some of the newer HTTP features work out of the box
|
||||
for macOS users (starting with HTTPie 0.9.4.).
|
||||
* Added the ability to unset a request header with ``Header:``, and send an
|
||||
empty value with ``Header;``.
|
||||
* Added ``--default-scheme <URL_SCHEME>`` to enable things like
|
||||
``$ alias https='http --default-scheme=https``.
|
||||
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
|
||||
* Added fish shell completion (located in ``extras/httpie-completion.fish``
|
||||
in the Github repo).
|
||||
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
|
||||
``pip install requests[socks]``.
|
||||
* Changed the default JSON ``Accept`` header from ``application/json``
|
||||
to ``application/json, */*``.
|
||||
* Changed the pre-processing of request HTTP headers so that any leading
|
||||
and trailing whitespace is removed.
|
||||
|
||||
|
||||
`0.9.4`_ (2016-07-01)
|
||||
---------------------
|
||||
@ -289,4 +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.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
|
||||
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.4...master
|
||||
.. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.4...0.9.6
|
||||
.. _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
|
||||
|
10
Makefile
10
Makefile
@ -57,10 +57,12 @@ test-bdist-wheel: clean uninstall-httpie
|
||||
test-all: uninstall-all clean init test test-tox test-dist
|
||||
|
||||
|
||||
publish: test-all
|
||||
publish: test-all publish-no-test
|
||||
|
||||
publish-no-test:
|
||||
@echo $(TAG)Testing wheel build an installation$(END)
|
||||
@echo "$(VERSION)"
|
||||
@echo "$(VERSION)" | grep -q "dev" && echo "!!!Not publishing dev version!!!" && exit 1
|
||||
@echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok
|
||||
python setup.py register
|
||||
python setup.py sdist upload
|
||||
python setup.py bdist_wheel upload
|
||||
@ -92,3 +94,7 @@ uninstall-all: uninstall-httpie
|
||||
|
||||
@echo $(TAG)Uninstalling development requirements$(END)
|
||||
- pip uninstall --yes -r $(REQUIREMENTS)
|
||||
|
||||
|
||||
homebrew-formula-vars:
|
||||
extras/get-homebrew-formula-vars.py
|
||||
|
726
README.rst
726
README.rst
File diff suppressed because it is too large
Load Diff
55
extras/get-homebrew-formula-vars.py
Executable file
55
extras/get-homebrew-formula-vars.py
Executable file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Generate URLs and file hashes to be included in the Homebrew formula
|
||||
after a new release of HTTPie is published on PyPi.
|
||||
|
||||
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
|
||||
"""
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
PACKAGES = [
|
||||
'httpie',
|
||||
'requests',
|
||||
'pygments',
|
||||
]
|
||||
|
||||
|
||||
def get_info(package_name):
|
||||
api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name)
|
||||
resp = requests.get(api_url).json()
|
||||
hasher = hashlib.sha256()
|
||||
for release in resp['urls']:
|
||||
download_url = release['url']
|
||||
if download_url.endswith('.tar.gz'):
|
||||
hasher.update(requests.get(download_url).content)
|
||||
return {
|
||||
'name': package_name,
|
||||
'url': download_url,
|
||||
'sha256': hasher.hexdigest(),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'{}: download not found: {}'.format(package_name, resp))
|
||||
|
||||
|
||||
packages = {
|
||||
package_name: get_info(package_name) for package_name in PACKAGES
|
||||
}
|
||||
|
||||
|
||||
httpie_info = packages.pop('httpie')
|
||||
print("""
|
||||
url "{url}"
|
||||
sha256 "{sha256}"
|
||||
""".format(**httpie_info))
|
||||
|
||||
|
||||
for package_info in packages.values():
|
||||
print("""
|
||||
resource "{name}" do
|
||||
url "{url}"
|
||||
sha256 "{sha256}"
|
||||
end""".format(**package_info))
|
60
extras/httpie-completion.fish
Normal file
60
extras/httpie-completion.fish
Normal file
@ -0,0 +1,60 @@
|
||||
function __fish_httpie_auth_types
|
||||
echo "basic"\t"Basic HTTP auth"
|
||||
echo "digest"\t"Digest HTTP auth"
|
||||
end
|
||||
|
||||
function __fish_httpie_styles
|
||||
echo "autumn"
|
||||
echo "borland"
|
||||
echo "bw"
|
||||
echo "colorful"
|
||||
echo "default"
|
||||
echo "emacs"
|
||||
echo "friendly"
|
||||
echo "fruity"
|
||||
echo "igor"
|
||||
echo "manni"
|
||||
echo "monokai"
|
||||
echo "murphy"
|
||||
echo "native"
|
||||
echo "paraiso-dark"
|
||||
echo "paraiso-light"
|
||||
echo "pastie"
|
||||
echo "perldoc"
|
||||
echo "rrt"
|
||||
echo "solarized"
|
||||
echo "tango"
|
||||
echo "trac"
|
||||
echo "vim"
|
||||
echo "vs"
|
||||
echo "xcode"
|
||||
end
|
||||
|
||||
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode"
|
||||
complete -c http -s f -l form -d 'Data items from the command line are serialized as form fields'
|
||||
complete -c http -s j -l json -d '(default) Data items from the command line are serialized as a JSON object'
|
||||
complete -x -c http -l pretty -d 'Controls output processing' -a "all colors format none" -A
|
||||
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode"
|
||||
complete -x -c http -s p -l print -d 'String specifying what the output should contain'
|
||||
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
|
||||
complete -c http -s h -l headers -d 'Print only the response headers'
|
||||
complete -c http -s b -l body -d 'Print only the response body'
|
||||
complete -c http -s S -l stream -d 'Always stream the output by line'
|
||||
complete -c http -s o -l output -d 'Save output to FILE'
|
||||
complete -c http -s d -l download -d 'Do not print the response body to stdout'
|
||||
complete -c http -s c -l continue -d 'Resume an interrupted download'
|
||||
complete -x -c http -l session -d 'Create, or reuse and update a session'
|
||||
complete -x -c http -s a -l auth -d 'If only the username is provided (-a username), HTTPie will prompt for the password'
|
||||
complete -x -c http -l auth-type -d 'The authentication mechanism to be used' -a '(__fish_httpie_auth_types)' -A
|
||||
complete -x -c http -l proxy -d 'String mapping protocol to the URL of the proxy'
|
||||
complete -c http -l follow -d 'Allow full redirects'
|
||||
complete -x -c http -l verify -d 'SSL cert verification'
|
||||
complete -c http -l cert -d 'SSL cert'
|
||||
complete -c http -l cert-key -d 'Private SSL cert key'
|
||||
complete -x -c http -l timeout -d 'Connection timeout in seconds'
|
||||
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
|
||||
complete -c http -l ignore-stdin -d 'Do not attempt to read stdin'
|
||||
complete -c http -l help -d 'Show help'
|
||||
complete -c http -l version -d 'Show version'
|
||||
complete -c http -l traceback -d 'Prints exception traceback should one occur'
|
||||
complete -c http -l debug -d 'Show debugging information'
|
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'
|
||||
__version__ = '0.9.4'
|
||||
__version__ = '0.9.8'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
@ -11,6 +11,11 @@ class ExitStatus:
|
||||
"""Exit status code constants."""
|
||||
OK = 0
|
||||
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_TOO_MANY_REDIRECTS = 6
|
||||
|
||||
|
@ -3,8 +3,16 @@
|
||||
|
||||
"""
|
||||
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__':
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
@ -3,24 +3,27 @@
|
||||
NOTE: the CLI interface may change before reaching v1.0.
|
||||
|
||||
"""
|
||||
from textwrap import dedent, wrap
|
||||
# noinspection PyCompatibility
|
||||
from argparse import (RawDescriptionHelpFormatter, FileType,
|
||||
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
|
||||
from argparse import (
|
||||
RawDescriptionHelpFormatter, FileType,
|
||||
OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
)
|
||||
from textwrap import dedent, wrap
|
||||
|
||||
from httpie import __doc__, __version__
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
from httpie.input import (
|
||||
HTTPieArgumentParser, KeyValueArgType,
|
||||
SEP_PROXY, SEP_GROUP_ALL_ITEMS,
|
||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
||||
readable_file_arg, SSL_VERSION_ARG_MAPPING
|
||||
)
|
||||
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_RESP_BODY, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
||||
readable_file_arg, SSL_VERSION_ARG_MAPPING)
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
|
||||
|
||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
@ -41,6 +44,7 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
formatter_class=HTTPieHelpFormatter,
|
||||
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||
@ -89,6 +93,7 @@ positional.add_argument(
|
||||
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
|
||||
|
||||
@ -101,6 +106,7 @@ positional.add_argument(
|
||||
'items',
|
||||
metavar='REQUEST_ITEM',
|
||||
nargs=ZERO_OR_MORE,
|
||||
default=None,
|
||||
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
||||
help=r"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
@ -212,7 +218,7 @@ output_processing.add_argument(
|
||||
""".format(
|
||||
default=DEFAULT_STYLE,
|
||||
available='\n'.join(
|
||||
'{0}{1}'.format(8*' ', line.strip())
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
)
|
||||
@ -412,8 +418,8 @@ sessions.add_argument(
|
||||
auth = parser.add_argument_group(title='Authentication')
|
||||
auth.add_argument(
|
||||
'--auth', '-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS]',
|
||||
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
||||
help="""
|
||||
If only the username is provided (-a username), HTTPie will prompt
|
||||
for the password.
|
||||
@ -421,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.add_argument(
|
||||
'--auth-type', '-A',
|
||||
choices=[plugin.auth_type for plugin in _auth_plugins],
|
||||
default=_auth_plugins[0].auth_type,
|
||||
choices=_AuthTypeLazyChoices(),
|
||||
default=None,
|
||||
help="""
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
@ -576,7 +593,7 @@ ssl.add_argument(
|
||||
troubleshooting = parser.add_argument_group(title='Troubleshooting')
|
||||
|
||||
troubleshooting.add_argument(
|
||||
'--ignore-stdin',
|
||||
'--ignore-stdin', '-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
@ -611,6 +628,14 @@ troubleshooting.add_argument(
|
||||
|
||||
"""
|
||||
)
|
||||
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',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import json
|
||||
import sys
|
||||
from pprint import pformat
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
@ -24,8 +23,9 @@ except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
JSON = 'application/json'
|
||||
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE)
|
||||
DEFAULT_UA = 'HTTPie/%s' % __version__
|
||||
|
||||
|
||||
@ -85,13 +85,23 @@ def dump_request(kwargs):
|
||||
% repr_dict_nice(kwargs))
|
||||
|
||||
|
||||
def encode_headers(headers):
|
||||
# This allows for unicode headers which is non-standard but practical.
|
||||
# See: https://github.com/jkbrzt/httpie/issues/212
|
||||
return dict(
|
||||
(name, value.encode('utf8') if isinstance(value, str) else value)
|
||||
for name, value in headers.items()
|
||||
)
|
||||
def finalize_headers(headers):
|
||||
final_headers = {}
|
||||
for name, value in headers.items():
|
||||
if value is not None:
|
||||
|
||||
# >leading or trailing LWS MAY be removed without
|
||||
# >changing the semantics of the field value"
|
||||
# -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
|
||||
# Also, requests raises `InvalidHeader` for leading spaces.
|
||||
value = value.strip()
|
||||
|
||||
if isinstance(value, str):
|
||||
# See: https://github.com/jkbrzt/httpie/issues/212
|
||||
value = value.encode('utf8')
|
||||
|
||||
final_headers[name] = value
|
||||
return final_headers
|
||||
|
||||
|
||||
def get_default_headers(args):
|
||||
@ -100,16 +110,15 @@ def get_default_headers(args):
|
||||
}
|
||||
|
||||
auto_json = args.data and not args.form
|
||||
# FIXME: Accept is set to JSON with `http url @./file.txt`.
|
||||
if args.json or auto_json:
|
||||
default_headers['Accept'] = 'application/json'
|
||||
default_headers['Accept'] = JSON_ACCEPT
|
||||
if args.json or (auto_json and args.data):
|
||||
default_headers['Content-Type'] = JSON
|
||||
default_headers['Content-Type'] = JSON_CONTENT_TYPE
|
||||
|
||||
elif args.form and not args.files:
|
||||
# If sending files, `requests` will set
|
||||
# the `Content-Type` for us.
|
||||
default_headers['Content-Type'] = FORM
|
||||
default_headers['Content-Type'] = FORM_CONTENT_TYPE
|
||||
return default_headers
|
||||
|
||||
|
||||
@ -134,12 +143,7 @@ def get_requests_kwargs(args, base_headers=None):
|
||||
if base_headers:
|
||||
headers.update(base_headers)
|
||||
headers.update(args.headers)
|
||||
headers = encode_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)
|
||||
headers = finalize_headers(headers)
|
||||
|
||||
cert = None
|
||||
if args.cert:
|
||||
@ -159,7 +163,7 @@ def get_requests_kwargs(args, base_headers=None):
|
||||
}.get(args.verify, args.verify),
|
||||
'cert': cert,
|
||||
'timeout': args.timeout,
|
||||
'auth': credentials,
|
||||
'auth': args.auth,
|
||||
'proxies': dict((p.key, p.value) for p in args.proxy),
|
||||
'files': args.files,
|
||||
'allow_redirects': args.follow,
|
||||
|
@ -14,10 +14,14 @@ is_windows = 'win32' in str(sys.platform).lower()
|
||||
|
||||
|
||||
if is_py2:
|
||||
# noinspection PyShadowingBuiltins
|
||||
bytes = str
|
||||
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
|
||||
str = unicode
|
||||
elif is_py3:
|
||||
# noinspection PyShadowingBuiltins
|
||||
str = str
|
||||
# noinspection PyShadowingBuiltins
|
||||
bytes = bytes
|
||||
|
||||
|
||||
@ -32,7 +36,7 @@ try: # pragma: no cover
|
||||
# noinspection PyCompatibility
|
||||
from urllib.request import urlopen
|
||||
except ImportError: # pragma: no cover
|
||||
# noinspection PyCompatibility
|
||||
# noinspection PyCompatibility,PyUnresolvedReferences
|
||||
from urllib2 import urlopen
|
||||
|
||||
try: # pragma: no cover
|
||||
@ -40,10 +44,10 @@ try: # pragma: no cover
|
||||
except ImportError: # pragma: no cover
|
||||
# Python 2.6 OrderedDict class, needed for headers, parameters, etc .###
|
||||
# <https://pypi.python.org/pypi/ordereddict/1.1>
|
||||
# noinspection PyCompatibility
|
||||
# noinspection PyCompatibility,PyUnresolvedReferences
|
||||
from UserDict import DictMixin
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
# noinspection PyShadowingBuiltins,PyCompatibility
|
||||
class OrderedDict(dict, DictMixin):
|
||||
# Copyright (c) 2009 Raymond Hettinger
|
||||
#
|
||||
@ -115,6 +119,7 @@ except ImportError: # pragma: no cover
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
# noinspection PyUnresolvedReferences
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
|
@ -80,7 +80,7 @@ class BaseConfigDict(dict):
|
||||
class Config(BaseConfigDict):
|
||||
|
||||
name = 'config'
|
||||
helpurl = 'https://github.com/jkbrzt/httpie#config'
|
||||
helpurl = 'https://httpie.org/docs#config'
|
||||
about = 'HTTPie configuration file'
|
||||
|
||||
DEFAULTS = {
|
||||
|
@ -1,4 +1,8 @@
|
||||
import sys
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
curses = None # Compiled w/o curses
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.config import DEFAULT_CONFIG_DIR, Config
|
||||
@ -28,17 +32,12 @@ class Environment(object):
|
||||
stderr_isatty = stderr.isatty()
|
||||
colors = 256
|
||||
if not is_windows:
|
||||
import curses
|
||||
try:
|
||||
curses.setupterm()
|
||||
if curses:
|
||||
try:
|
||||
curses.setupterm()
|
||||
colors = curses.tigetnum('colors')
|
||||
except TypeError:
|
||||
# pypy3 (2.4.0)
|
||||
colors = curses.tigetnum(b'colors')
|
||||
except curses.error:
|
||||
pass
|
||||
del curses
|
||||
except curses.error:
|
||||
pass
|
||||
else:
|
||||
# noinspection PyUnresolvedReferences
|
||||
import colorama.initialise
|
||||
|
@ -212,7 +212,7 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
exit_status = ExitStatus.ERROR_CTRL_C
|
||||
except SystemExit as e:
|
||||
if e.code != ExitStatus.OK:
|
||||
env.stderr.write('\n')
|
||||
@ -230,7 +230,7 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
exit_status = ExitStatus.ERROR_CTRL_C
|
||||
except SystemExit as e:
|
||||
if e.code != ExitStatus.OK:
|
||||
env.stderr.write('\n')
|
||||
@ -243,7 +243,7 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
except requests.TooManyRedirects:
|
||||
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
||||
log_error('Too many redirects (--max-redirects=%s).',
|
||||
parsed_args.max_redirects)
|
||||
parsed_args.max_redirects)
|
||||
except Exception as e:
|
||||
# TODO: Further distinction between expected and unexpected errors.
|
||||
msg = str(e)
|
||||
|
@ -78,15 +78,15 @@ def parse_content_range(content_range, resumed_from):
|
||||
# last-byte-pos value, is invalid. The recipient of an invalid
|
||||
# byte-content-range- spec MUST ignore it and any content
|
||||
# transferred along with it."
|
||||
if (first_byte_pos >= last_byte_pos
|
||||
or (instance_length is not None
|
||||
and instance_length <= last_byte_pos)):
|
||||
if (first_byte_pos >= last_byte_pos or
|
||||
(instance_length is not None and
|
||||
instance_length <= last_byte_pos)):
|
||||
raise ContentRangeError(
|
||||
'Invalid Content-Range returned: %r' % content_range)
|
||||
|
||||
if (first_byte_pos != resumed_from
|
||||
or (instance_length is not None
|
||||
and last_byte_pos + 1 != instance_length)):
|
||||
if (first_byte_pos != resumed_from or
|
||||
(instance_length is not None and
|
||||
last_byte_pos + 1 != instance_length)):
|
||||
# Not what we asked for.
|
||||
raise ContentRangeError(
|
||||
'Unexpected Content-Range returned (%r)'
|
||||
@ -308,9 +308,9 @@ class Downloader(object):
|
||||
@property
|
||||
def interrupted(self):
|
||||
return (
|
||||
self.finished
|
||||
and self.status.total_size
|
||||
and self.status.total_size != self.status.downloaded
|
||||
self.finished and
|
||||
self.status.total_size and
|
||||
self.status.total_size != self.status.downloaded
|
||||
)
|
||||
|
||||
def chunk_downloaded(self, chunk):
|
||||
@ -399,8 +399,8 @@ class ProgressReporterThread(threading.Thread):
|
||||
if now - self._prev_time >= self._update_interval:
|
||||
downloaded = self.status.downloaded
|
||||
try:
|
||||
speed = ((downloaded - self._prev_bytes)
|
||||
/ (now - self._prev_time))
|
||||
speed = ((downloaded - self._prev_bytes) /
|
||||
(now - self._prev_time))
|
||||
except ZeroDivisionError:
|
||||
speed = 0
|
||||
|
||||
@ -434,11 +434,11 @@ class ProgressReporterThread(threading.Thread):
|
||||
self._prev_bytes = downloaded
|
||||
|
||||
self.output.write(
|
||||
CLEAR_LINE
|
||||
+ ' '
|
||||
+ SPINNER[self._spinner_pos]
|
||||
+ ' '
|
||||
+ self._status_line
|
||||
CLEAR_LINE +
|
||||
' ' +
|
||||
SPINNER[self._spinner_pos] +
|
||||
' ' +
|
||||
self._status_line
|
||||
)
|
||||
self.output.flush()
|
||||
|
||||
@ -447,8 +447,8 @@ class ProgressReporterThread(threading.Thread):
|
||||
else 0)
|
||||
|
||||
def sum_up(self):
|
||||
actually_downloaded = (self.status.downloaded
|
||||
- self.status.resumed_from)
|
||||
actually_downloaded = (
|
||||
self.status.downloaded - self.status.resumed_from)
|
||||
time_taken = self.status.time_finished - self.status.time_started
|
||||
|
||||
self.output.write(CLEAR_LINE)
|
||||
@ -463,8 +463,8 @@ class ProgressReporterThread(threading.Thread):
|
||||
|
||||
self.output.write(SUMMARY.format(
|
||||
downloaded=humanize_bytes(actually_downloaded),
|
||||
total=(self.status.total_size
|
||||
and humanize_bytes(self.status.total_size)),
|
||||
total=(self.status.total_size and
|
||||
humanize_bytes(self.status.total_size)),
|
||||
speed=humanize_bytes(speed),
|
||||
time=time_taken,
|
||||
))
|
||||
|
105
httpie/input.py
105
httpie/input.py
@ -15,6 +15,7 @@ from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||
|
||||
# TODO: Use MultiDict for headers once added to `requests`.
|
||||
# https://github.com/jkbrzt/httpie/issues/130
|
||||
from httpie.plugins import plugin_manager
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27
|
||||
@ -28,12 +29,11 @@ URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
HTTP = 'http://'
|
||||
HTTPS = 'https://'
|
||||
|
||||
|
||||
# Various separators used in args
|
||||
SEP_HEADERS = ':'
|
||||
SEP_HEADERS_EMPTY = ';'
|
||||
SEP_CREDENTIALS = ':'
|
||||
SEP_PROXY = ':'
|
||||
SEP_DATA = '='
|
||||
@ -67,6 +67,7 @@ SEP_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
# Separators allowed in ITEM arguments
|
||||
SEP_GROUP_ALL_ITEMS = frozenset([
|
||||
SEP_HEADERS,
|
||||
SEP_HEADERS_EMPTY,
|
||||
SEP_QUERY,
|
||||
SEP_DATA,
|
||||
SEP_DATA_RAW_JSON,
|
||||
@ -151,7 +152,7 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
if not self.args.ignore_stdin and not env.stdin_isatty:
|
||||
self._body_from_file(self.env.stdin)
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
scheme = HTTP
|
||||
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)
|
||||
@ -214,31 +215,58 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
def _process_auth(self):
|
||||
"""
|
||||
If only a username provided via --auth, then ask for a password.
|
||||
Or, take credentials from the URL, if provided.
|
||||
|
||||
"""
|
||||
# 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:
|
||||
if not self.args.auth.has_password():
|
||||
# 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)
|
||||
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=SEP_CREDENTIALS,
|
||||
orig=SEP_CREDENTIALS.join([username, password])
|
||||
)
|
||||
|
||||
elif 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=SEP_CREDENTIALS,
|
||||
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):
|
||||
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||
@ -309,9 +337,10 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
self.args.url = self.args.method
|
||||
# Infer the method
|
||||
has_data = (
|
||||
(not self.args.ignore_stdin and not self.env.stdin_isatty)
|
||||
or any(item.sep in SEP_GROUP_DATA_ITEMS
|
||||
for item in self.args.items)
|
||||
(not self.args.ignore_stdin and
|
||||
not self.env.stdin_isatty) or
|
||||
any(item.sep in SEP_GROUP_DATA_ITEMS
|
||||
for item in self.args.items)
|
||||
)
|
||||
self.args.method = HTTP_POST if has_data else HTTP_GET
|
||||
|
||||
@ -439,8 +468,8 @@ class SessionNameValidator(object):
|
||||
|
||||
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)):
|
||||
if (os.path.sep not in value and
|
||||
not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||
raise ArgumentError(None, self.error_message)
|
||||
return value
|
||||
|
||||
@ -577,6 +606,9 @@ class AuthCredentialsArgType(KeyValueArgType):
|
||||
)
|
||||
|
||||
|
||||
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
|
||||
|
||||
|
||||
class RequestItemsDict(OrderedDict):
|
||||
"""Multi-value dict for URL parameters and form data."""
|
||||
|
||||
@ -655,11 +687,20 @@ def parse_items(items,
|
||||
data = []
|
||||
files = []
|
||||
params = []
|
||||
|
||||
for item in items:
|
||||
value = item.value
|
||||
|
||||
if item.sep == SEP_HEADERS:
|
||||
if value == '':
|
||||
# No value => unset the header
|
||||
value = None
|
||||
target = headers
|
||||
elif item.sep == SEP_HEADERS_EMPTY:
|
||||
if item.value:
|
||||
raise ParseError(
|
||||
'Invalid item "%s" '
|
||||
'(to specify an empty header use `Header;`)'
|
||||
% item.orig
|
||||
)
|
||||
target = headers
|
||||
elif item.sep == SEP_QUERY:
|
||||
target = params
|
||||
|
@ -17,13 +17,39 @@ class AuthPlugin(BasePlugin):
|
||||
|
||||
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
|
||||
# to use this auth plugin. Eg. "my-auth"
|
||||
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.
|
||||
|
||||
"""
|
||||
|
@ -5,6 +5,7 @@ import requests.auth
|
||||
from httpie.plugins.base import AuthPlugin
|
||||
|
||||
|
||||
# noinspection PyAbstractClass
|
||||
class BuiltinAuthPlugin(AuthPlugin):
|
||||
|
||||
package_name = '(builtin)'
|
||||
@ -35,6 +36,7 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
|
||||
name = 'Basic HTTP auth'
|
||||
auth_type = 'basic'
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def get_auth(self, username, password):
|
||||
return HTTPBasicAuth(username, password)
|
||||
|
||||
@ -44,5 +46,6 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
|
||||
name = 'Digest HTTP auth'
|
||||
auth_type = 'digest'
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def get_auth(self, username, password):
|
||||
return requests.auth.HTTPDigestAuth(username, password)
|
||||
|
@ -24,6 +24,9 @@ class PluginManager(object):
|
||||
for plugin in plugins:
|
||||
self._plugins.append(plugin)
|
||||
|
||||
def unregister(self, plugin):
|
||||
self._plugins.remove(plugin)
|
||||
|
||||
def load_installed_plugins(self):
|
||||
for entry_point_name in ENTRY_POINT_NAMES:
|
||||
for entry_point in iter_entry_points(entry_point_name):
|
||||
|
@ -51,11 +51,10 @@ def get_response(requests_session, session_name,
|
||||
dump_request(kwargs)
|
||||
session.update_headers(kwargs['headers'])
|
||||
|
||||
if args.auth:
|
||||
if args.auth_plugin:
|
||||
session.auth = {
|
||||
'type': args.auth_type,
|
||||
'username': args.auth.key,
|
||||
'password': args.auth.value,
|
||||
'type': args.auth_plugin.auth_type,
|
||||
'raw_auth': args.auth_plugin.raw_auth,
|
||||
}
|
||||
elif session.auth:
|
||||
kwargs['auth'] = session.auth
|
||||
@ -75,7 +74,7 @@ def get_response(requests_session, session_name,
|
||||
|
||||
|
||||
class Session(BaseConfigDict):
|
||||
helpurl = 'https://github.com/jkbrzt/httpie#sessions'
|
||||
helpurl = 'https://httpie.org/docs#sessions'
|
||||
about = 'HTTPie session file'
|
||||
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
@ -147,10 +146,31 @@ class Session(BaseConfigDict):
|
||||
auth = self.get('auth', None)
|
||||
if not auth or not auth['type']:
|
||||
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
|
||||
def auth(self, auth):
|
||||
assert set(['type', 'username', 'password']) == set(auth.keys())
|
||||
assert set(['type', 'raw_auth']) == set(auth.keys())
|
||||
self['auth'] = auth
|
||||
|
6
setup.py
6
setup.py
@ -35,10 +35,11 @@ tests_require = [
|
||||
|
||||
|
||||
install_requires = [
|
||||
'requests>=2.3.0',
|
||||
'Pygments>=1.5'
|
||||
'requests>=2.11.0',
|
||||
'Pygments>=2.1.3'
|
||||
]
|
||||
|
||||
|
||||
# Conditional dependencies:
|
||||
|
||||
# sdist
|
||||
@ -68,6 +69,7 @@ def long_description():
|
||||
with codecs.open('README.rst', encoding='utf8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
setup(
|
||||
name='httpie',
|
||||
version=httpie.__version__,
|
||||
|
@ -60,5 +60,16 @@ def test_only_username_in_url(url):
|
||||
"""
|
||||
args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
|
||||
assert args.auth
|
||||
assert args.auth.key == 'username'
|
||||
assert args.auth.value == ''
|
||||
assert args.auth.username == 'username'
|
||||
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."""
|
||||
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
|
||||
from httpie.compat import urlopen
|
||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||
from utils import TestEnvironment, http
|
||||
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
|
||||
|
||||
|
||||
class TestBinaryRequestData:
|
||||
|
@ -68,10 +68,11 @@ class TestItemParsing:
|
||||
def test_valid_items(self):
|
||||
items = input.parse_items([
|
||||
self.key_value('string=value'),
|
||||
self.key_value('header:value'),
|
||||
self.key_value('Header:value'),
|
||||
self.key_value('Unset-Header:'),
|
||||
self.key_value('Empty-Header;'),
|
||||
self.key_value('list:=["a", 1, {}, false]'),
|
||||
self.key_value('obj:={"a": "b"}'),
|
||||
self.key_value('eh:'),
|
||||
self.key_value('ed='),
|
||||
self.key_value('bool:=true'),
|
||||
self.key_value('file@' + FILE_PATH_ARG),
|
||||
@ -83,7 +84,11 @@ class TestItemParsing:
|
||||
# Parsed headers
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(items.headers._store.values())
|
||||
assert headers == {'header': 'value', 'eh': ''}
|
||||
assert headers == {
|
||||
'Header': 'value',
|
||||
'Unset-Header': None,
|
||||
'Empty-Header': ''
|
||||
}
|
||||
|
||||
# Parsed data
|
||||
raw_json_embed = items.data.pop('raw-json-embed')
|
||||
@ -103,8 +108,8 @@ class TestItemParsing:
|
||||
|
||||
# Parsed file fields
|
||||
assert 'file' in items.files
|
||||
assert (items.files['file'][1].read().strip().decode('utf8')
|
||||
== FILE_CONTENT)
|
||||
assert (items.files['file'][1].read().strip().
|
||||
decode('utf8') == FILE_CONTENT)
|
||||
|
||||
def test_multiple_file_fields_with_same_field_name(self):
|
||||
items = input.parse_items([
|
||||
@ -325,8 +330,18 @@ class TestIgnoreStdin:
|
||||
|
||||
class TestSchemes:
|
||||
|
||||
def test_custom_scheme(self):
|
||||
def test_invalid_custom_scheme(self):
|
||||
# InvalidSchema is expected because HTTPie
|
||||
# shouldn't touch a formally valid scheme.
|
||||
with pytest.raises(InvalidSchema):
|
||||
http('foo+bar-BAZ.123://bah')
|
||||
|
||||
def test_invalid_scheme_via_via_default_scheme(self):
|
||||
# InvalidSchema is expected because HTTPie
|
||||
# shouldn't touch a formally valid scheme.
|
||||
with pytest.raises(InvalidSchema):
|
||||
http('bah', '--default=scheme=foo+bar-BAZ.123')
|
||||
|
||||
def test_default_scheme(self, httpbin_secure):
|
||||
url = '{0}:{1}'.format(httpbin_secure.host, httpbin_secure.port)
|
||||
assert HTTP_OK in http(url, '--default-scheme=https')
|
||||
|
@ -2,6 +2,7 @@
|
||||
Tests for the provided defaults regarding HTTP method, and --json vs. --form.
|
||||
|
||||
"""
|
||||
from httpie.client import JSON_ACCEPT
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH
|
||||
|
||||
@ -58,20 +59,20 @@ class TestAutoContentTypeAndAcceptHeaders:
|
||||
def test_POST_with_data_auto_JSON_headers(self, httpbin):
|
||||
r = http('POST', httpbin.url + '/post', 'a=b')
|
||||
assert HTTP_OK in r
|
||||
assert '"Accept": "application/json"' in r
|
||||
assert '"Content-Type": "application/json' in r
|
||||
assert r.json['headers']['Accept'] == JSON_ACCEPT
|
||||
assert r.json['headers']['Content-Type'] == 'application/json'
|
||||
|
||||
def test_GET_with_data_auto_JSON_headers(self, httpbin):
|
||||
# JSON headers should automatically be set also for GET with data.
|
||||
r = http('POST', httpbin.url + '/post', 'a=b')
|
||||
assert HTTP_OK in r
|
||||
assert '"Accept": "application/json"' in r, r
|
||||
assert '"Content-Type": "application/json' in r
|
||||
assert r.json['headers']['Accept'] == JSON_ACCEPT
|
||||
assert r.json['headers']['Content-Type'] == 'application/json'
|
||||
|
||||
def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin):
|
||||
r = http('--json', 'POST', httpbin.url + '/post')
|
||||
assert HTTP_OK in r
|
||||
assert r.json['headers']['Accept'] == 'application/json'
|
||||
assert r.json['headers']['Accept'] == JSON_ACCEPT
|
||||
# Make sure Content-Type gets set even with no data.
|
||||
# https://github.com/jkbrzt/httpie/issues/137
|
||||
assert 'application/json' in r.json['headers']['Content-Type']
|
||||
|
@ -1,9 +1,25 @@
|
||||
import mock
|
||||
|
||||
from httpie import ExitStatus
|
||||
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):
|
||||
r = http('GET', httpbin.url + '/status/200')
|
||||
r = http('GET', httpbin.url + '/get')
|
||||
assert HTTP_OK in r
|
||||
assert r.exit_status == ExitStatus.OK
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""High-level tests."""
|
||||
import pytest
|
||||
|
||||
from httpie.input import ParseError
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH, FILE_CONTENT
|
||||
|
||||
@ -75,6 +77,36 @@ def test_headers(httpbin_both):
|
||||
assert '"Foo": "bar"' in r
|
||||
|
||||
|
||||
def test_headers_unset(httpbin_both):
|
||||
r = http('GET', httpbin_both + '/headers')
|
||||
assert 'Accept' in r.json['headers'] # default Accept present
|
||||
|
||||
r = http('GET', httpbin_both + '/headers', 'Accept:')
|
||||
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):
|
||||
r = http('GET', httpbin_both + '/headers')
|
||||
assert r.json['headers']['Accept'] # default Accept has value
|
||||
|
||||
r = http('GET', httpbin_both + '/headers', 'Accept;')
|
||||
assert r.json['headers']['Accept'] == '' # Accept has no value
|
||||
|
||||
|
||||
def test_headers_empty_value_with_value_gives_error(httpbin):
|
||||
with pytest.raises(ParseError):
|
||||
http('GET', httpbin + '/headers', 'Accept;SYNTAX_ERROR')
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_py26,
|
||||
reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only'
|
||||
|
@ -192,7 +192,7 @@ def http(*args, **kwargs):
|
||||
args_with_config_defaults = args + env.config.default_options
|
||||
add_to_args = []
|
||||
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')
|
||||
if not any('--timeout' in arg for arg in args_with_config_defaults):
|
||||
add_to_args.append('--timeout=3')
|
||||
|
10
tox.ini
10
tox.ini
@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
[tox]
|
||||
envlist = py26, py27, py35, pypy
|
||||
envlist = py26, py27, py35, pypy, codestyle
|
||||
|
||||
|
||||
[testenv]
|
||||
@ -20,3 +20,11 @@ commands =
|
||||
--verbose \
|
||||
--doctest-modules \
|
||||
{posargs:./httpie ./tests}
|
||||
|
||||
[testenv:codestyle]
|
||||
deps = pycodestyle
|
||||
commands =
|
||||
pycodestyle \
|
||||
--ignore=E241,E501
|
||||
# 241 - multiple spaces after ‘,’
|
||||
# 501 - line too long
|
||||
|
Reference in New Issue
Block a user