mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 10:27:47 +02:00
Compare commits
190 Commits
Author | SHA1 | Date | |
---|---|---|---|
0eba037037 | |||
3898129e9c | |||
b88e88d2e3 | |||
d1407baf76 | |||
d5032ca859 | |||
f6a19cf552 | |||
74979f3b33 | |||
698eb51e60 | |||
ae8030c930 | |||
2e96d7ffbb | |||
b5625e3d75 | |||
932d3224f4 | |||
b596fedf13 | |||
96444f3345 | |||
89b66f1608 | |||
a7d570916d | |||
ab5a50cee8 | |||
91961c6b51 | |||
256ea7d49d | |||
2cd6ea3050 | |||
37dddf5bf7 | |||
e508c631f2 | |||
55530c8c6d | |||
eb929cbc04 | |||
2490bb25ca | |||
2038fa02e3 | |||
59d51ad513 | |||
61568f1def | |||
f93f4fa7c7 | |||
bf73b5701e | |||
7917f1b40c | |||
a50660cc70 | |||
749b1e2aca | |||
137889a267 | |||
c9c6f0fae5 | |||
6fd1ea0e5a | |||
8f7676a2a9 | |||
87e661c5f1 | |||
8ca333dda0 | |||
0f4dce98c7 | |||
05547224ce | |||
6301fee3d2 | |||
a803e845a5 | |||
11be041e06 | |||
7f5fd130c5 | |||
ec899d70b7 | |||
4d3b4fa0be | |||
27c557e983 | |||
7f24f7d34c | |||
4b61108005 | |||
8b189725fd | |||
1719ebded6 | |||
c5d6a4ad8e | |||
91e1fe2d0f | |||
ca7f41de53 | |||
46e24dd6b5 | |||
803127e8c9 | |||
4c138959ea | |||
91a28973bd | |||
02b28093a8 | |||
d64e7d8a6a | |||
8841b8bf46 | |||
6472ca55e1 | |||
37c3307018 | |||
0aab796960 | |||
95c33e31a2 | |||
9af833da30 | |||
dfe6245cd6 | |||
555761f3cb | |||
643735ef23 | |||
7a45f14542 | |||
e993f83355 | |||
d726a4cd92 | |||
8d3f09497b | |||
31c78c2885 | |||
9776a6dea0 | |||
f1d4861fae | |||
d99e1ff492 | |||
a196d1d451 | |||
02209c2db1 | |||
9886f01f91 | |||
a4f796fe69 | |||
c948f98b05 | |||
b0fde07cfd | |||
f74670fac1 | |||
7321b9fa4e | |||
cf8d5eb3e8 | |||
64af72eb88 | |||
de38f86730 | |||
244ad15c92 | |||
586f45e634 | |||
b1b4743663 | |||
5600b4a2d3 | |||
9261167a1f | |||
519654e21b | |||
4840499a43 | |||
ee6cdf4ab3 | |||
98003f545d | |||
0046ed73c6 | |||
66a6475064 | |||
97804802c0 | |||
c9296a9a45 | |||
64a41c2601 | |||
0af6ae1be4 | |||
d0fc10cf1a | |||
fe1d0b0a1e | |||
f133dbf22c | |||
9d93b07a9d | |||
761cdbf8be | |||
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 | |||
6b06d92a59 | |||
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 |
@ -1 +0,0 @@
|
||||
; needs to exist otherwise `$ coveralls` fails
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,12 +2,12 @@
|
||||
.idea/
|
||||
__pycache__/
|
||||
dist/
|
||||
httpie.egg-info/
|
||||
build/
|
||||
*.egg-info
|
||||
.cache/
|
||||
.tox
|
||||
.tox/
|
||||
.coverage
|
||||
*.pyc
|
||||
*.egg
|
||||
htmlcov
|
||||
.pytest_cache/
|
||||
|
119
.travis.yml
119
.travis.yml
@ -1,79 +1,96 @@
|
||||
# https://travis-ci.org/jkbrzt/httpie
|
||||
# <https://travis-ci.org/jakubroztocil/httpie>
|
||||
sudo: false
|
||||
|
||||
language: python
|
||||
os:
|
||||
- linux
|
||||
|
||||
env:
|
||||
global:
|
||||
- NEWEST_PYTHON=3.5
|
||||
|
||||
- NEWEST_PYTHON=3.7
|
||||
python:
|
||||
- 2.6
|
||||
# <https://docs.travis-ci.com/user/languages/python/>
|
||||
|
||||
- 2.7
|
||||
- pypy
|
||||
- 3.4
|
||||
|
||||
# Python 3.4 fails installing packages
|
||||
# <https://travis-ci.org/jakubroztocil/httpie/jobs/403263566#L636>
|
||||
# - 3.4
|
||||
|
||||
- 3.5
|
||||
- pypy3
|
||||
- 3.6
|
||||
# - 3.7 # is done in the matrix below as described in travis-ci/travis-ci#9069
|
||||
- pypy
|
||||
|
||||
# pypy3 currently fails because of a Flask issue
|
||||
# - pypy3
|
||||
|
||||
cache: pip
|
||||
matrix:
|
||||
|
||||
include:
|
||||
|
||||
# Manually defined OS X builds
|
||||
# https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)
|
||||
|
||||
# Stock OSX Python
|
||||
# Add manually defined OS X builds
|
||||
# <https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)>
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
# Stock OSX Python
|
||||
- TOXENV=py27-osx-builtin
|
||||
- BREW_PYTHON_PACKAGE=
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
# Latest Python 2.7 from Homebrew
|
||||
- TOXENV=py27
|
||||
|
||||
# Latest Python 2.x from Homebrew
|
||||
- BREW_PYTHON_PACKAGE=python@2
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
- TOXENV=py27
|
||||
- BREW_INSTALL=python
|
||||
|
||||
# Latest Python 3.x from Homebrew
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
- TOXENV=py35
|
||||
- BREW_INSTALL=python3
|
||||
|
||||
# Latest Python 3.x from Homebrew
|
||||
- TOXENV=py37 # <= needs to be kept up-to-date to reflect latest minor version
|
||||
- BREW_PYTHON_PACKAGE=python@3
|
||||
# Travis Python 3.7 must run sudo on
|
||||
- os: linux
|
||||
python: 3.7
|
||||
env: TOXENV=py37
|
||||
sudo: true # Required for Python 3.7
|
||||
dist: xenial # Required for Python 3.7
|
||||
# Add a codestyle-only build
|
||||
- os: linux
|
||||
python: 3.6
|
||||
env: CODESTYLE_ONLY=true
|
||||
install:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
|
||||
if [[ -n "$BREW_INSTALL" ]]; then
|
||||
brew update
|
||||
brew install "$BREW_INSTALL"
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
|
||||
if [[ -n "$BREW_PYTHON_PACKAGE" ]]; then
|
||||
brew update
|
||||
if ! brew list --versions "$BREW_PYTHON_PACKAGE" >/dev/null; then
|
||||
brew install "$BREW_PYTHON_PACKAGE"
|
||||
elif ! brew outdated "$BREW_PYTHON_PACKAGE"; then
|
||||
brew upgrade "$BREW_PYTHON_PACKAGE"
|
||||
fi
|
||||
fi
|
||||
sudo pip2 install tox
|
||||
fi
|
||||
sudo pip install tox
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
make
|
||||
else
|
||||
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
|
||||
fi
|
||||
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
if [[ $CODESTYLE_ONLY ]]; then
|
||||
make pycodestyle
|
||||
else
|
||||
make test
|
||||
fi
|
||||
else
|
||||
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
|
||||
fi
|
||||
after_success:
|
||||
- |
|
||||
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
pip install python-coveralls && coveralls
|
||||
fi
|
||||
|
||||
- |
|
||||
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
make coveralls
|
||||
fi
|
||||
notifications:
|
||||
|
||||
webhooks:
|
||||
# options: [always|never|change] default: always
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: always
|
||||
urls:
|
||||
# https://gitter.im/jkbrzt/httpie
|
||||
- https://webhooks.gitter.im/e/c42fcd359a110d02830b
|
||||
on_success: always # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: always # options: [always|never|change] default: always
|
||||
|
@ -2,13 +2,13 @@
|
||||
HTTPie authors
|
||||
==============
|
||||
|
||||
* `Jakub Roztocil <https://github.com/jkbrzt>`_
|
||||
* `Jakub Roztocil <https://github.com/jakubroztocil>`_
|
||||
|
||||
|
||||
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/jakubroztocil/httpie/graphs/contributors>`_
|
||||
|
||||
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
|
||||
* `Hank Gay <https://github.com/gthank>`_
|
||||
|
130
CHANGELOG.rst
130
CHANGELOG.rst
@ -6,9 +6,75 @@ 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.3-dev`_ (unreleased)
|
||||
-------------------------
|
||||
|
||||
* No changes yet.
|
||||
|
||||
|
||||
`1.0.2`_ (2018-11-14)
|
||||
-------------------------
|
||||
|
||||
* Fixed tests for installation with pyOpenSSL.
|
||||
|
||||
|
||||
`1.0.1`_ (2018-11-14)
|
||||
-------------------------
|
||||
|
||||
* Removed external URL calls from tests.
|
||||
|
||||
|
||||
`1.0.0`_ (2018-11-02)
|
||||
-------------------------
|
||||
|
||||
* Added ``--style=auto`` which follows the terminal ANSI color styles.
|
||||
* Added support for selecting TLS 1.3 via ``--ssl=tls1.3``
|
||||
(available once implemented in upstream libraries).
|
||||
* Added ``true``/``false`` as valid values for ``--verify``
|
||||
(in addition to ``yes``/``no``) and the boolean value is case-insensitive.
|
||||
* Changed the default ``--style`` from ``solarized`` to ``auto`` (on Windows it stays ``fruity``).
|
||||
* Fixed default headers being incorrectly case-sensitive.
|
||||
* Removed Python 2.6 support.
|
||||
|
||||
|
||||
|
||||
`0.9.9`_ (2016-12-08)
|
||||
---------------------
|
||||
|
||||
* Fixed README.
|
||||
|
||||
|
||||
`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)
|
||||
---------------------
|
||||
@ -61,8 +127,8 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
||||
---------------------
|
||||
|
||||
* Added support for Requests transport adapter plugins
|
||||
(see `httpie-unixsocket <https://github.com/msabramo/httpie-unixsocket>`_
|
||||
and `httpie-http2 <https://github.com/jkbrzt/httpie-http2>`_)
|
||||
(see `httpie-unixsocket <https://github.com/httpie/httpie-unixsocket>`_
|
||||
and `httpie-http2 <https://github.com/httpie/httpie-http2>`_)
|
||||
|
||||
|
||||
`0.9.0`_ (2015-01-31)
|
||||
@ -260,33 +326,39 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
||||
* Many improvements and bug fixes
|
||||
|
||||
|
||||
`0.1`_ (2012-02-25)
|
||||
-------------------
|
||||
`0.1.0`_ (2012-02-25)
|
||||
---------------------
|
||||
|
||||
* Initial public release
|
||||
|
||||
|
||||
.. _`0.1`: https://github.com/jkbrzt/httpie/commit/b966efa
|
||||
.. _0.1.4: https://github.com/jkbrzt/httpie/compare/b966efa...0.1.4
|
||||
.. _0.1.5: https://github.com/jkbrzt/httpie/compare/0.1.4...0.1.5
|
||||
.. _0.1.6: https://github.com/jkbrzt/httpie/compare/0.1.5...0.1.6
|
||||
.. _0.2.0: https://github.com/jkbrzt/httpie/compare/0.1.6...0.2.0
|
||||
.. _0.2.1: https://github.com/jkbrzt/httpie/compare/0.2.0...0.2.1
|
||||
.. _0.2.2: https://github.com/jkbrzt/httpie/compare/0.2.1...0.2.2
|
||||
.. _0.2.5: https://github.com/jkbrzt/httpie/compare/0.2.2...0.2.5
|
||||
.. _0.2.6: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.6
|
||||
.. _0.2.7: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.7
|
||||
.. _0.3.0: https://github.com/jkbrzt/httpie/compare/0.2.7...0.3.0
|
||||
.. _0.4.0: https://github.com/jkbrzt/httpie/compare/0.3.0...0.4.0
|
||||
.. _0.4.1: https://github.com/jkbrzt/httpie/compare/0.4.0...0.4.1
|
||||
.. _0.5.0: https://github.com/jkbrzt/httpie/compare/0.4.1...0.5.0
|
||||
.. _0.5.1: https://github.com/jkbrzt/httpie/compare/0.5.0...0.5.1
|
||||
.. _0.6.0: https://github.com/jkbrzt/httpie/compare/0.5.1...0.6.0
|
||||
.. _0.7.1: https://github.com/jkbrzt/httpie/compare/0.6.0...0.7.1
|
||||
.. _0.8.0: https://github.com/jkbrzt/httpie/compare/0.7.1...0.8.0
|
||||
.. _0.9.0: https://github.com/jkbrzt/httpie/compare/0.8.0...0.9.0
|
||||
.. _0.9.1: https://github.com/jkbrzt/httpie/compare/0.9.0...0.9.1
|
||||
.. _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.1.0`: https://github.com/jakubroztocil/httpie/commit/b966efa
|
||||
.. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4
|
||||
.. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5
|
||||
.. _0.1.6: https://github.com/jakubroztocil/httpie/compare/0.1.5...0.1.6
|
||||
.. _0.2.0: https://github.com/jakubroztocil/httpie/compare/0.1.6...0.2.0
|
||||
.. _0.2.1: https://github.com/jakubroztocil/httpie/compare/0.2.0...0.2.1
|
||||
.. _0.2.2: https://github.com/jakubroztocil/httpie/compare/0.2.1...0.2.2
|
||||
.. _0.2.5: https://github.com/jakubroztocil/httpie/compare/0.2.2...0.2.5
|
||||
.. _0.2.6: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.6
|
||||
.. _0.2.7: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.7
|
||||
.. _0.3.0: https://github.com/jakubroztocil/httpie/compare/0.2.7...0.3.0
|
||||
.. _0.4.0: https://github.com/jakubroztocil/httpie/compare/0.3.0...0.4.0
|
||||
.. _0.4.1: https://github.com/jakubroztocil/httpie/compare/0.4.0...0.4.1
|
||||
.. _0.5.0: https://github.com/jakubroztocil/httpie/compare/0.4.1...0.5.0
|
||||
.. _0.5.1: https://github.com/jakubroztocil/httpie/compare/0.5.0...0.5.1
|
||||
.. _0.6.0: https://github.com/jakubroztocil/httpie/compare/0.5.1...0.6.0
|
||||
.. _0.7.1: https://github.com/jakubroztocil/httpie/compare/0.6.0...0.7.1
|
||||
.. _0.8.0: https://github.com/jakubroztocil/httpie/compare/0.7.1...0.8.0
|
||||
.. _0.9.0: https://github.com/jakubroztocil/httpie/compare/0.8.0...0.9.0
|
||||
.. _0.9.1: https://github.com/jakubroztocil/httpie/compare/0.9.0...0.9.1
|
||||
.. _0.9.2: https://github.com/jakubroztocil/httpie/compare/0.9.1...0.9.2
|
||||
.. _0.9.3: https://github.com/jakubroztocil/httpie/compare/0.9.2...0.9.3
|
||||
.. _0.9.4: https://github.com/jakubroztocil/httpie/compare/0.9.3...0.9.4
|
||||
.. _0.9.6: https://github.com/jakubroztocil/httpie/compare/0.9.4...0.9.6
|
||||
.. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8
|
||||
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9
|
||||
.. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0
|
||||
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1
|
||||
.. _1.0.2: https://github.com/jakubroztocil/httpie/compare/1.0.1...1.0.2
|
||||
.. _1.0.3-dev: https://github.com/jakubroztocil/httpie/compare/1.0.2...master
|
||||
|
@ -25,14 +25,14 @@ to your bug report, e.g.:
|
||||
|
||||
Before working on a new feature or a bug, please browse `existing issues`_
|
||||
to see whether it has been previously discussed. If the change in question
|
||||
is a bigger one, it's always good to discuss before your starting working on
|
||||
is a bigger one, it's always good to discuss before you start working on
|
||||
it.
|
||||
|
||||
|
||||
Creating Development Environment
|
||||
--------------------------------
|
||||
|
||||
Go to https://github.com/jkbrzt/httpie and fork the project repository.
|
||||
Go to https://github.com/jakubroztocil/httpie and fork the project repository.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
@ -47,13 +47,14 @@ Go to https://github.com/jkbrzt/httpie and fork the project repository.
|
||||
|
||||
# Install dev. requirements and also HTTPie (in editable mode
|
||||
# so that the `http' command will point to your working copy):
|
||||
make
|
||||
make init
|
||||
|
||||
|
||||
Making Changes
|
||||
--------------
|
||||
|
||||
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8).
|
||||
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8)
|
||||
and that ``make pycodestyle`` passes.
|
||||
|
||||
|
||||
Testing
|
||||
@ -71,18 +72,18 @@ Running all tests:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run all tests on the current Python interpreter
|
||||
# Run all tests on the current Python interpreter with coverage
|
||||
make test
|
||||
|
||||
# Run all tests on the current Python with coverage
|
||||
make test-cover
|
||||
|
||||
# Run all tests in all of the supported and available Pythons via Tox
|
||||
make test-tox
|
||||
|
||||
# Run all tests for code as well as packaging, etc.
|
||||
make test-all
|
||||
|
||||
# Test PEP8 compliance
|
||||
make pycodestyle
|
||||
|
||||
|
||||
Running specific tests:
|
||||
***********************
|
||||
@ -95,11 +96,11 @@ Running specific tests:
|
||||
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
|
||||
|
||||
# Run specific tests on the on all Pythons via Tox
|
||||
# (change to `tox -e py37' to limit Python version)
|
||||
tox -- tests/test_uploads.py --verbose
|
||||
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
|
||||
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
|
||||
|
||||
|
||||
-----
|
||||
|
||||
See `Makefile`_ for additional development utilities.
|
||||
@ -107,10 +108,10 @@ Don't forget to add yourself to `AUTHORS`_!
|
||||
|
||||
|
||||
.. _Tox: http://tox.testrun.org
|
||||
.. _supported Python environments: https://github.com/jkbrzt/httpie/blob/master/tox.ini
|
||||
.. _existing issues: https://github.com/jkbrzt/httpie/issues?state=open
|
||||
.. _AUTHORS: https://github.com/jkbrzt/httpie/blob/master/AUTHORS.rst
|
||||
.. _Makefile: https://github.com/jkbrzt/httpie/blob/master/Makefile
|
||||
.. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini
|
||||
.. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open
|
||||
.. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst
|
||||
.. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile
|
||||
.. _pytest: http://pytest.org/
|
||||
.. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/
|
||||
.. _test suite: https://github.com/jkbrzt/httpie/tree/master/tests
|
||||
.. _test suite: https://github.com/jakubroztocil/httpie/tree/master/tests
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright © 2012-2016 Jakub Roztocil <jakub@roztocil.co>
|
||||
Copyright © 2012-2017 Jakub Roztocil <jakub@roztocil.co>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
82
Makefile
82
Makefile
@ -1,6 +1,6 @@
|
||||
#
|
||||
###############################################################################
|
||||
# See ./CONTRIBUTING.rst
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
VERSION=$(shell grep __version__ httpie/__init__.py)
|
||||
REQUIREMENTS="requirements-dev.txt"
|
||||
@ -20,6 +20,17 @@ init: uninstall-httpie
|
||||
|
||||
@echo
|
||||
|
||||
clean:
|
||||
@echo $(TAG)Cleaning up$(END)
|
||||
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
|
||||
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
|
||||
@echo
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Testing
|
||||
###############################################################################
|
||||
|
||||
|
||||
test: init
|
||||
@echo $(TAG)Running tests on the current Python interpreter with coverage $(END)
|
||||
@ -27,9 +38,8 @@ test: init
|
||||
@echo
|
||||
|
||||
|
||||
test-tox: init
|
||||
@echo $(TAG)Running tests on all Pythons via Tox$(END)
|
||||
tox
|
||||
# test-all is meant to test everything — even this Makefile
|
||||
test-all: uninstall-all clean init test test-tox test-dist pycodestyle
|
||||
@echo
|
||||
|
||||
|
||||
@ -37,6 +47,12 @@ test-dist: test-sdist test-bdist-wheel
|
||||
@echo
|
||||
|
||||
|
||||
test-tox: init
|
||||
@echo $(TAG)Running tests on all Pythons via Tox$(END)
|
||||
tox
|
||||
@echo
|
||||
|
||||
|
||||
test-sdist: clean uninstall-httpie
|
||||
@echo $(TAG)Testing sdist build an installation$(END)
|
||||
python setup.py sdist
|
||||
@ -53,26 +69,40 @@ test-bdist-wheel: clean uninstall-httpie
|
||||
@echo
|
||||
|
||||
|
||||
# This tests everything, even this Makefile.
|
||||
test-all: uninstall-all clean init test test-tox test-dist
|
||||
pycodestyle:
|
||||
which pycodestyle || pip install pycodestyle
|
||||
pycodestyle
|
||||
@echo
|
||||
|
||||
|
||||
publish: test-all
|
||||
coveralls:
|
||||
which coveralls || pip install python-coveralls
|
||||
coveralls
|
||||
@echo
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Publishing to PyPi
|
||||
###############################################################################
|
||||
|
||||
|
||||
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
|
||||
@echo
|
||||
|
||||
|
||||
clean:
|
||||
@echo $(TAG)Cleaning up$(END)
|
||||
rm -rf .tox *.egg dist build .coverage
|
||||
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
|
||||
@echo
|
||||
|
||||
###############################################################################
|
||||
# Uninstalling
|
||||
###############################################################################
|
||||
|
||||
uninstall-httpie:
|
||||
@echo $(TAG)Uninstalling httpie$(END)
|
||||
@ -92,3 +122,27 @@ uninstall-all: uninstall-httpie
|
||||
|
||||
@echo $(TAG)Uninstalling development requirements$(END)
|
||||
- pip uninstall --yes -r $(REQUIREMENTS)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Docs
|
||||
###############################################################################
|
||||
|
||||
pdf:
|
||||
# NOTE: rst2pdf needs to be installed manually and against a Python 2
|
||||
@echo "Converting README.rst to PDF…"
|
||||
rst2pdf \
|
||||
--strip-elements-with-class=no-pdf \
|
||||
README.rst \
|
||||
-o README.pdf
|
||||
@echo "Done"
|
||||
@echo
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Utils
|
||||
###############################################################################
|
||||
|
||||
|
||||
homebrew-formula-vars:
|
||||
extras/get-homebrew-formula-vars.py
|
||||
|
876
README.rst
876
README.rst
File diff suppressed because it is too large
Load Diff
23
appveyor.yml
23
appveyor.yml
@ -1,23 +0,0 @@
|
||||
# https://ci.appveyor.com/project/jkbrzt/httpie
|
||||
build: false
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- PYTHON: "C:/Python27"
|
||||
# Python 3.4 has outdated pip
|
||||
# - PYTHON: "C:/Python34"
|
||||
- PYTHON: "C:/Python35"
|
||||
|
||||
init:
|
||||
- "ECHO %PYTHON%"
|
||||
- ps: "ls C:/Python*"
|
||||
|
||||
install:
|
||||
# FIXME: updating pip fails with PermissionError
|
||||
# - "%PYTHON%/Scripts/pip.exe install -U pip setuptools"
|
||||
- "%PYTHON%/Scripts/pip.exe install -e ."
|
||||
|
||||
test_script:
|
||||
- "%PYTHON%/Scripts/pip.exe --version"
|
||||
- "%PYTHON%/Scripts/http.exe --debug"
|
||||
- "%PYTHON%/python.exe setup.py test"
|
62
extras/get-homebrew-formula-vars.py
Executable file
62
extras/get-homebrew-formula-vars.py
Executable file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate URLs and file hashes to be included in the Homebrew formula
|
||||
after a new release of HTTPie has been published on PyPi.
|
||||
|
||||
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
|
||||
"""
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
PACKAGES = [
|
||||
'httpie',
|
||||
'pygments',
|
||||
'requests',
|
||||
'certifi',
|
||||
'urllib3',
|
||||
'idna',
|
||||
'chardet',
|
||||
'PySocks',
|
||||
]
|
||||
|
||||
|
||||
def get_package_meta(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))
|
||||
|
||||
|
||||
def main():
|
||||
package_meta_map = {
|
||||
package_name: get_package_meta(package_name)
|
||||
for package_name in PACKAGES
|
||||
}
|
||||
httpie_meta = package_meta_map.pop('httpie')
|
||||
print()
|
||||
print(' url "{url}"'.format(url=httpie_meta['url']))
|
||||
print(' sha256 "{sha256}"'.format(sha256=httpie_meta['sha256']))
|
||||
print()
|
||||
for dep_meta in package_meta_map.values():
|
||||
print(' resource "{name}" do'.format(name=dep_meta['name']))
|
||||
print(' url "{url}"'.format(url=dep_meta['url']))
|
||||
print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256']))
|
||||
print(' end')
|
||||
print('')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
59
extras/httpie-completion.fish
Normal file
59
extras/httpie-completion.fish
Normal file
@ -0,0 +1,59 @@
|
||||
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 '(__fish_httpie_styles)'
|
||||
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 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'
|
69
extras/httpie.rb
Normal file
69
extras/httpie.rb
Normal file
@ -0,0 +1,69 @@
|
||||
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
|
||||
# Only useful for testing until it gets accepted by homebrew maintainers.
|
||||
# (It will need to be updated from the repo version before next release.)
|
||||
#
|
||||
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
#
|
||||
class Httpie < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.org/"
|
||||
url "https://files.pythonhosted.org/packages/44/ee/7177b743400d7f82a69bf30cb3c24ea4bb1f4aea68878bc540f732bf4940/httpie-1.0.0.tar.gz"
|
||||
sha256 "1650342d2eca2622092196bf106ab8f68ea2dbb2ed265d37191185618e159a25"
|
||||
head "https://github.com/jakubroztocil/httpie.git"
|
||||
|
||||
bottle do
|
||||
cellar :any_skip_relocation
|
||||
sha256 "7e9db255e324dd63b66106ca62ed7e4e81f6634c624dec3ff49c293aba1072a6" => :mojave
|
||||
sha256 "437504a11416284b17d3a801c267d0fd5e15416f38cff3abf7ed99b096b4828a" => :high_sierra
|
||||
sha256 "10b25fc787076719b1f1f9c242c5e9d872ebd1c7a6d83e6f1af983a17cd8ca55" => :sierra
|
||||
sha256 "1bd35480d1ef401bdad9c322e7c1624aefc9b5056530ab990e327d0bc397e4fb" => :el_capitan
|
||||
end
|
||||
|
||||
depends_on "python" ["3.6.5_1"]
|
||||
|
||||
resource "pygments" do
|
||||
url "https://files.pythonhosted.org/packages/71/2a/2e4e77803a8bd6408a2903340ac498cb0a2181811af7c9ec92cb70b0308a/Pygments-2.2.0.tar.gz"
|
||||
sha256 "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/97/10/92d25b93e9c266c94b76a5548f020f3f1dd0eb40649cb1993532c0af8f4c/requests-2.20.0.tar.gz"
|
||||
sha256 "99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c"
|
||||
end
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/41/b6/4f0cefba47656583217acd6cd797bc2db1fede0d53090fdc28ad2c8e0716/certifi-2018.10.15.tar.gz"
|
||||
sha256 "6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/a5/74/05ffd00b4b5c08306939c485869f5dc40cbc27357195b0a98b18e4c48893/urllib3-1.24.tar.gz"
|
||||
sha256 "41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz"
|
||||
sha256 "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
end
|
||||
|
||||
resource "chardet" do
|
||||
url "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz"
|
||||
sha256 "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
url "https://files.pythonhosted.org/packages/53/12/6bf1d764f128636cef7408e8156b7235b150ea31650d0260969215bb8e7d/PySocks-1.6.8.tar.gz"
|
||||
sha256 "3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672"
|
||||
end
|
||||
|
||||
def install
|
||||
virtualenv_install_with_resources
|
||||
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
|
BIN
httpie.png
BIN
httpie.png
Binary file not shown.
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 681 KiB |
@ -2,15 +2,20 @@
|
||||
HTTPie - a CLI, cURL-like tool for humans.
|
||||
|
||||
"""
|
||||
__version__ = '1.0.2'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.9.4'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
class ExitStatus:
|
||||
"""Exit status code constants."""
|
||||
OK = 0
|
||||
"""Program exit code constants."""
|
||||
SUCCESS = 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
|
||||
|
||||
@ -20,8 +25,8 @@ class ExitStatus:
|
||||
ERROR_HTTP_5XX = 5
|
||||
|
||||
|
||||
EXIT_STATUS_LABELS = dict(
|
||||
(value, key)
|
||||
EXIT_STATUS_LABELS = {
|
||||
value: key
|
||||
for key, value in ExitStatus.__dict__.items()
|
||||
if key.isupper()
|
||||
)
|
||||
}
|
||||
|
@ -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,29 @@
|
||||
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.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, AUTO_STYLE
|
||||
)
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
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)
|
||||
|
||||
|
||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
@ -41,7 +46,9 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
prog='http',
|
||||
formatter_class=HTTPieHelpFormatter,
|
||||
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||
epilog=dedent("""
|
||||
@ -50,7 +57,7 @@ parser = HTTPieArgumentParser(
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
|
||||
https://github.com/jkbrzt/httpie/issues
|
||||
https://github.com/jakubroztocil/httpie/issues
|
||||
|
||||
"""),
|
||||
)
|
||||
@ -89,6 +96,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 +109,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
|
||||
@ -203,18 +212,21 @@ output_processing.add_argument(
|
||||
help="""
|
||||
Output coloring style (default is "{default}"). One of:
|
||||
|
||||
{available}
|
||||
{available_styles}
|
||||
|
||||
For this option to work properly, please make sure that the $TERM
|
||||
environment variable is set to "xterm-256color" or similar
|
||||
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='\n'.join(
|
||||
'{0}{1}'.format(8*' ', line.strip())
|
||||
available_styles='\n'.join(
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
)
|
||||
|
||||
@ -412,8 +424,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 +433,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}".
|
||||
|
||||
@ -527,10 +550,10 @@ ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
help="""
|
||||
Set to "no" to skip checking the host's SSL certificate. You can also pass
|
||||
the path to a CA_BUNDLE file for private certs. You can also set the
|
||||
REQUESTS_CA_BUNDLE environment variable. Defaults to "yes".
|
||||
|
||||
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(
|
||||
@ -576,7 +599,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 +634,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,10 +1,9 @@
|
||||
import json
|
||||
import sys
|
||||
from pprint import pformat
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages import urllib3
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from httpie import sessions
|
||||
from httpie import __version__
|
||||
@ -15,8 +14,10 @@ from httpie.utils import repr_dict_nice
|
||||
|
||||
try:
|
||||
# https://urllib3.readthedocs.io/en/latest/security.html
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
except AttributeError:
|
||||
except (ImportError, AttributeError):
|
||||
# In some rare cases, the user may have an old version of the requests
|
||||
# or urllib3, and there is no method called "disable_warnings." In these
|
||||
# cases, we don't need to call the method.
|
||||
@ -24,8 +25,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,31 +87,40 @@ 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/jakubroztocil/httpie/issues/212
|
||||
value = value.encode('utf8')
|
||||
|
||||
final_headers[name] = value
|
||||
return final_headers
|
||||
|
||||
|
||||
def get_default_headers(args):
|
||||
default_headers = {
|
||||
default_headers = CaseInsensitiveDict({
|
||||
'User-Agent': DEFAULT_UA
|
||||
}
|
||||
})
|
||||
|
||||
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 +145,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:
|
||||
@ -155,12 +161,14 @@ def get_requests_kwargs(args, base_headers=None):
|
||||
'data': data,
|
||||
'verify': {
|
||||
'yes': True,
|
||||
'no': False
|
||||
}.get(args.verify, args.verify),
|
||||
'true': True,
|
||||
'no': False,
|
||||
'false': False,
|
||||
}.get(args.verify.lower(), args.verify),
|
||||
'cert': cert,
|
||||
'timeout': args.timeout,
|
||||
'auth': credentials,
|
||||
'proxies': dict((p.key, p.value) for p in args.proxy),
|
||||
'auth': args.auth,
|
||||
'proxies': {p.key: p.value for p in args.proxy},
|
||||
'files': args.files,
|
||||
'allow_redirects': args.follow,
|
||||
'params': args.params,
|
||||
|
146
httpie/compat.py
146
httpie/compat.py
@ -1,12 +1,11 @@
|
||||
"""
|
||||
Python 2.6, 2.7, and 3.x compatibility.
|
||||
Python 2.7, and 3.x compatibility.
|
||||
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
is_py2 = sys.version_info[0] == 2
|
||||
is_py26 = sys.version_info[:2] == (2, 6)
|
||||
is_py27 = sys.version_info[:2] == (2, 7)
|
||||
is_py3 = sys.version_info[0] == 3
|
||||
is_pypy = 'pypy' in sys.version.lower()
|
||||
@ -14,10 +13,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,142 +35,5 @@ 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
|
||||
from collections import OrderedDict
|
||||
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
|
||||
from UserDict import DictMixin
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
class OrderedDict(dict, DictMixin):
|
||||
# Copyright (c) 2009 Raymond Hettinger
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d'
|
||||
% len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
# noinspection PyUnusedLocal
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return self.__class__, (items,), inst_dict
|
||||
return self.__class__, (items,)
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
if len(self) != len(other):
|
||||
return False
|
||||
for p, q in zip(self.items(), other.items()):
|
||||
if p != q:
|
||||
return False
|
||||
return True
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
@ -80,7 +80,7 @@ class BaseConfigDict(dict):
|
||||
class Config(BaseConfigDict):
|
||||
|
||||
name = 'config'
|
||||
helpurl = 'https://github.com/jkbrzt/httpie#config'
|
||||
helpurl = 'https://httpie.org/doc#config'
|
||||
about = 'HTTPie configuration file'
|
||||
|
||||
DEFAULTS = {
|
||||
@ -104,7 +104,7 @@ class Config(BaseConfigDict):
|
||||
try:
|
||||
implicit_content_type = self.pop('implicit_content_type')
|
||||
except KeyError:
|
||||
pass
|
||||
self.save()
|
||||
else:
|
||||
if implicit_content_type == 'form':
|
||||
self['default_options'].insert(0, '--form')
|
||||
|
@ -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
|
||||
|
@ -43,7 +43,7 @@ def get_exit_status(http_status, follow=False):
|
||||
# Server Error
|
||||
return ExitStatus.ERROR_HTTP_5XX
|
||||
else:
|
||||
return ExitStatus.OK
|
||||
return ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def print_debug_info(env):
|
||||
@ -61,7 +61,7 @@ def print_debug_info(env):
|
||||
|
||||
def decode_args(args, stdin_encoding):
|
||||
"""
|
||||
Convert all bytes ags to str
|
||||
Convert all bytes args to str
|
||||
by decoding them using stdin encoding.
|
||||
|
||||
"""
|
||||
@ -82,7 +82,7 @@ def program(args, env, log_error):
|
||||
:return: status code
|
||||
|
||||
"""
|
||||
exit_status = ExitStatus.OK
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
downloader = None
|
||||
show_traceback = args.debug or args.traceback
|
||||
|
||||
@ -109,7 +109,7 @@ def program(args, env, log_error):
|
||||
http_status=response.status_code,
|
||||
follow=args.follow
|
||||
)
|
||||
if not env.stdout_isatty and exit_status != ExitStatus.OK:
|
||||
if not env.stdout_isatty and exit_status != ExitStatus.SUCCESS:
|
||||
log_error(
|
||||
'HTTP %s %s', response.raw.status, response.raw.reason,
|
||||
level='warning'
|
||||
@ -143,7 +143,7 @@ def program(args, env, log_error):
|
||||
else:
|
||||
raise
|
||||
|
||||
if downloader and exit_status == ExitStatus.OK:
|
||||
if downloader and exit_status == ExitStatus.SUCCESS:
|
||||
# Last response body download.
|
||||
download_stream, download_to = downloader.start(final_response)
|
||||
write_stream(
|
||||
@ -164,8 +164,8 @@ def program(args, env, log_error):
|
||||
if downloader and not downloader.finished:
|
||||
downloader.failed()
|
||||
|
||||
if (not isinstance(args, list) and args.output_file and
|
||||
args.output_file_specified):
|
||||
if (not isinstance(args, list) and args.output_file
|
||||
and args.output_file_specified):
|
||||
args.output_file.close()
|
||||
|
||||
|
||||
@ -202,9 +202,9 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
if include_debug_info:
|
||||
print_debug_info(env)
|
||||
if args == ['--debug']:
|
||||
return ExitStatus.OK
|
||||
return ExitStatus.SUCCESS
|
||||
|
||||
exit_status = ExitStatus.OK
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
|
||||
try:
|
||||
parsed_args = parser.parse_args(args=args, env=env)
|
||||
@ -212,9 +212,9 @@ 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:
|
||||
if e.code != ExitStatus.SUCCESS:
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
raise
|
||||
@ -230,9 +230,9 @@ 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:
|
||||
if e.code != ExitStatus.SUCCESS:
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
raise
|
||||
@ -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)
|
||||
|
@ -54,8 +54,8 @@ def parse_content_range(content_range, resumed_from):
|
||||
raise ContentRangeError('Missing Content-Range')
|
||||
|
||||
pattern = (
|
||||
'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)'
|
||||
'/(\*|(?P<instance_length>\d+))$'
|
||||
r'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)'
|
||||
r'/(\*|(?P<instance_length>\d+))$'
|
||||
)
|
||||
match = re.match(pattern, content_range)
|
||||
|
||||
@ -85,8 +85,8 @@ def parse_content_range(content_range, resumed_from):
|
||||
'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)):
|
||||
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)'
|
||||
@ -105,7 +105,7 @@ def filename_from_content_disposition(content_disposition):
|
||||
:return: the filename if present and valid, otherwise `None`
|
||||
|
||||
"""
|
||||
# attachment; filename=jkbrzt-httpie-0.4.1-20-g40bd8f6.tar.gz
|
||||
# attachment; filename=jakubroztocil-httpie-0.4.1-20-g40bd8f6.tar.gz
|
||||
|
||||
msg = Message('Content-Disposition: %s' % content_disposition)
|
||||
filename = msg.get_filename()
|
||||
@ -238,7 +238,7 @@ class Downloader(object):
|
||||
assert not self.status.time_started
|
||||
|
||||
# FIXME: some servers still might sent Content-Encoding: gzip
|
||||
# <https://github.com/jkbrzt/httpie/issues/423>
|
||||
# <https://github.com/jakubroztocil/httpie/issues/423>
|
||||
try:
|
||||
total_size = int(response.headers['Content-Length'])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
@ -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)
|
||||
|
122
httpie/input.py
122
httpie/input.py
@ -9,15 +9,16 @@ import errno
|
||||
import mimetypes
|
||||
import getpass
|
||||
from io import BytesIO
|
||||
from collections import namedtuple, Iterable
|
||||
from collections import namedtuple, Iterable, OrderedDict
|
||||
# noinspection PyCompatibility
|
||||
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||
|
||||
# TODO: Use MultiDict for headers once added to `requests`.
|
||||
# https://github.com/jkbrzt/httpie/issues/130
|
||||
# https://github.com/jakubroztocil/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
|
||||
from httpie.compat import urlsplit, str, is_pypy, is_py27
|
||||
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||
from httpie.utils import load_json_preserve_order
|
||||
|
||||
@ -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,
|
||||
@ -110,12 +111,13 @@ SSL_VERSION_ARG_MAPPING = {
|
||||
'tls1': 'PROTOCOL_TLSv1',
|
||||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||||
}
|
||||
SSL_VERSION_ARG_MAPPING = dict(
|
||||
(cli_arg, getattr(ssl, ssl_constant))
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class HTTPieArgumentParser(ArgumentParser):
|
||||
@ -151,7 +153,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 +216,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
|
||||
@ -274,7 +303,8 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
"""
|
||||
if self.args.data:
|
||||
self.error('Request body (from stdin or a file) and request '
|
||||
'data (key=value) cannot be mixed.')
|
||||
'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):
|
||||
@ -310,8 +340,10 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
# 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)
|
||||
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
|
||||
|
||||
@ -396,8 +428,8 @@ class HTTPieArgumentParser(ArgumentParser):
|
||||
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):
|
||||
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
|
||||
@ -475,7 +507,7 @@ class KeyValueArgType(object):
|
||||
"""Represents an escaped character."""
|
||||
|
||||
def tokenize(string):
|
||||
"""Tokenize `string`. There are only two token types - strings
|
||||
r"""Tokenize `string`. There are only two token types - strings
|
||||
and escaped characters:
|
||||
|
||||
tokenize(r'foo\=bar\\baz')
|
||||
@ -577,6 +609,9 @@ class AuthCredentialsArgType(KeyValueArgType):
|
||||
)
|
||||
|
||||
|
||||
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
|
||||
|
||||
|
||||
class RequestItemsDict(OrderedDict):
|
||||
"""Multi-value dict for URL parameters and form data."""
|
||||
|
||||
@ -655,11 +690,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
|
||||
|
@ -9,21 +9,25 @@ import pygments.style
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||
from pygments.lexers.special import TextLexer
|
||||
from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.plugins import FormatterPlugin
|
||||
|
||||
|
||||
AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys())
|
||||
AVAILABLE_STYLES.add('solarized')
|
||||
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
DEFAULT_STYLE = AUTO_STYLE
|
||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
||||
if is_windows:
|
||||
# Colors on Windows via colorama don't look that
|
||||
# great and fruity seems to give the best result there
|
||||
# great and fruity seems to give the best result there.
|
||||
DEFAULT_STYLE = 'fruity'
|
||||
else:
|
||||
DEFAULT_STYLE = 'solarized'
|
||||
|
||||
|
||||
AVAILABLE_STYLES = set(pygments.styles.get_all_styles())
|
||||
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
|
||||
AVAILABLE_STYLES.add(AUTO_STYLE)
|
||||
|
||||
|
||||
class ColorFormatter(FormatterPlugin):
|
||||
@ -39,40 +43,56 @@ class ColorFormatter(FormatterPlugin):
|
||||
def __init__(self, env, explicit_json=False,
|
||||
color_scheme=DEFAULT_STYLE, **kwargs):
|
||||
super(ColorFormatter, self).__init__(**kwargs)
|
||||
|
||||
if not env.colors:
|
||||
self.enabled = False
|
||||
return
|
||||
|
||||
# --json, -j
|
||||
self.explicit_json = explicit_json
|
||||
|
||||
try:
|
||||
style_class = pygments.styles.get_style_by_name(color_scheme)
|
||||
except ClassNotFound:
|
||||
style_class = Solarized256Style
|
||||
|
||||
if env.colors == 256:
|
||||
fmt_class = Terminal256Formatter
|
||||
use_auto_style = color_scheme == AUTO_STYLE
|
||||
has_256_colors = env.colors == 256
|
||||
if use_auto_style or not has_256_colors:
|
||||
http_lexer = PygmentsHttpLexer()
|
||||
formatter = TerminalFormatter()
|
||||
else:
|
||||
fmt_class = TerminalFormatter
|
||||
self.formatter = fmt_class(style=style_class)
|
||||
http_lexer = SimplifiedHTTPLexer()
|
||||
formatter = Terminal256Formatter(
|
||||
style=self.get_style_class(color_scheme)
|
||||
)
|
||||
|
||||
self.explicit_json = explicit_json # --json
|
||||
self.formatter = formatter
|
||||
self.http_lexer = http_lexer
|
||||
|
||||
def format_headers(self, headers):
|
||||
return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
|
||||
return pygments.highlight(
|
||||
code=headers,
|
||||
lexer=self.http_lexer,
|
||||
formatter=self.formatter,
|
||||
).strip()
|
||||
|
||||
def format_body(self, body, mime):
|
||||
lexer = self.get_lexer(mime, body)
|
||||
lexer = self.get_lexer_for_body(mime, body)
|
||||
if lexer:
|
||||
body = pygments.highlight(body, lexer, self.formatter)
|
||||
body = pygments.highlight(
|
||||
code=body,
|
||||
lexer=lexer,
|
||||
formatter=self.formatter,
|
||||
)
|
||||
return body.strip()
|
||||
|
||||
def get_lexer(self, mime, body):
|
||||
def get_lexer_for_body(self, mime, body):
|
||||
return get_lexer(
|
||||
mime=mime,
|
||||
explicit_json=self.explicit_json,
|
||||
body=body,
|
||||
)
|
||||
|
||||
def get_style_class(self, color_scheme):
|
||||
try:
|
||||
return pygments.styles.get_style_by_name(color_scheme)
|
||||
except ClassNotFound:
|
||||
return Solarized256Style
|
||||
|
||||
|
||||
def get_lexer(mime, explicit_json=False, body=''):
|
||||
|
||||
@ -121,7 +141,7 @@ def get_lexer(mime, explicit_json=False, body=''):
|
||||
return lexer
|
||||
|
||||
|
||||
class HTTPLexer(pygments.lexer.RegexLexer):
|
||||
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
"""Simplified HTTP lexer for Pygments.
|
||||
|
||||
It only operates on headers and provides a stronger contrast between
|
||||
|
@ -15,8 +15,8 @@ class JSONFormatter(FormatterPlugin):
|
||||
'javascript',
|
||||
'text',
|
||||
]
|
||||
if (self.kwargs['explicit_json'] or
|
||||
any(token in mime for token in maybe_json)):
|
||||
if (self.kwargs['explicit_json']
|
||||
or any(token in mime for token in maybe_json)):
|
||||
try:
|
||||
obj = json.loads(body)
|
||||
except ValueError:
|
||||
|
@ -15,15 +15,41 @@ class AuthPlugin(BasePlugin):
|
||||
"""
|
||||
Base auth plugin class.
|
||||
|
||||
See <https://github.com/jkbrzt/httpie-ntlm> for an example auth plugin.
|
||||
See <https://github.com/httpie/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)'
|
||||
@ -16,7 +17,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
|
||||
"""
|
||||
Override username/password serialization to allow unicode.
|
||||
|
||||
See https://github.com/jkbrzt/httpie/issues/212
|
||||
See https://github.com/jakubroztocil/httpie/issues/212
|
||||
|
||||
"""
|
||||
r.headers['Authorization'] = type(self).make_header(
|
||||
@ -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):
|
||||
@ -36,8 +39,7 @@ class PluginManager(object):
|
||||
return [plugin for plugin in self if issubclass(plugin, AuthPlugin)]
|
||||
|
||||
def get_auth_plugin_mapping(self):
|
||||
return dict((plugin.auth_type, plugin)
|
||||
for plugin in self.get_auth_plugins())
|
||||
return {plugin.auth_type: plugin for plugin in self.get_auth_plugins()}
|
||||
|
||||
def get_auth_plugin(self, auth_type):
|
||||
return self.get_auth_plugin_mapping()[auth_type]
|
||||
|
@ -30,8 +30,8 @@ def get_response(requests_session, session_name,
|
||||
if os.path.sep in session_name:
|
||||
path = os.path.expanduser(session_name)
|
||||
else:
|
||||
hostname = (args.headers.get('Host', None) or
|
||||
urlsplit(args.url).netloc.split('@')[-1])
|
||||
hostname = (args.headers.get('Host', None)
|
||||
or urlsplit(args.url).netloc.split('@')[-1])
|
||||
if not hostname:
|
||||
# HACK/FIXME: httpie-unixsocket's URLs have no hostname.
|
||||
hostname = 'localhost'
|
||||
@ -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/doc#sessions'
|
||||
about = 'HTTPie session file'
|
||||
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
@ -137,20 +136,41 @@ class Session(BaseConfigDict):
|
||||
stored_attrs = ['value', 'path', 'secure', 'expires']
|
||||
self['cookies'] = {}
|
||||
for cookie in jar:
|
||||
self['cookies'][cookie.name] = dict(
|
||||
(attname, getattr(cookie, attname))
|
||||
self['cookies'][cookie.name] = {
|
||||
attname: getattr(cookie, attname)
|
||||
for attname in stored_attrs
|
||||
)
|
||||
}
|
||||
|
||||
@property
|
||||
def auth(self):
|
||||
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
|
||||
|
@ -1,12 +1,9 @@
|
||||
from __future__ import division
|
||||
import json
|
||||
|
||||
from httpie.compat import is_py26, OrderedDict
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def load_json_preserve_order(s):
|
||||
if is_py26:
|
||||
return json.loads(s)
|
||||
return json.loads(s, object_pairs_hook=OrderedDict)
|
||||
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
[pytest]
|
||||
norecursedirs = tests/fixtures
|
@ -5,3 +5,4 @@ pytest-cov
|
||||
pytest-httpbin>=0.0.6
|
||||
docutils
|
||||
wheel
|
||||
pycodestyle
|
||||
|
17
setup.cfg
17
setup.cfg
@ -1,2 +1,19 @@
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
|
||||
[tool:pytest]
|
||||
# <https://docs.pytest.org/en/latest/customize.html>
|
||||
norecursedirs = tests/fixtures
|
||||
|
||||
|
||||
[pycodestyle]
|
||||
# <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration>
|
||||
|
||||
exclude = .git,.idea,__pycache__,build,dist,.tox,.pytest_cache,*.egg-info
|
||||
|
||||
# <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes>
|
||||
# E241 - multiple spaces after ‘,’
|
||||
# E501 - line too long
|
||||
# W503 - line break before binary operator
|
||||
ignore = E241,E501,W503
|
||||
|
16
setup.py
16
setup.py
@ -35,10 +35,11 @@ tests_require = [
|
||||
|
||||
|
||||
install_requires = [
|
||||
'requests>=2.3.0',
|
||||
'Pygments>=1.5'
|
||||
'requests>=2.18.4',
|
||||
'Pygments>=2.1.3'
|
||||
]
|
||||
|
||||
|
||||
# Conditional dependencies:
|
||||
|
||||
# sdist
|
||||
@ -57,9 +58,7 @@ if 'bdist_wheel' not in sys.argv:
|
||||
# bdist_wheel
|
||||
extras_require = {
|
||||
# http://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies
|
||||
':python_version == "2.6"'
|
||||
' or python_version == "3.0"'
|
||||
' or python_version == "3.1" ': ['argparse>=1.2.1'],
|
||||
'python_version == "3.0" or python_version == "3.1"': ['argparse>=1.2.1'],
|
||||
':sys_platform == "win32"': ['colorama>=0.2.4'],
|
||||
}
|
||||
|
||||
@ -68,13 +67,14 @@ def long_description():
|
||||
with codecs.open('README.rst', encoding='utf8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
setup(
|
||||
name='httpie',
|
||||
version=httpie.__version__,
|
||||
description=httpie.__doc__.strip(),
|
||||
long_description=long_description(),
|
||||
url='http://httpie.org/',
|
||||
download_url='https://github.com/jkbrzt/httpie',
|
||||
download_url='https://github.com/jakubroztocil/httpie',
|
||||
author=httpie.__author__,
|
||||
author_email='jakub@roztocil.co',
|
||||
license=httpie.__licence__,
|
||||
@ -91,14 +91,14 @@ setup(
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.1',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
|
@ -5,4 +5,4 @@ HTTPie Test Suite
|
||||
Please see `CONTRIBUTING`_.
|
||||
|
||||
|
||||
.. _CONTRIBUTING: https://github.com/jkbrzt/httpie/blob/master/CONTRIBUTING.rst
|
||||
.. _CONTRIBUTING: https://github.com/jakubroztocil/httpie/blob/master/CONTRIBUTING.rst
|
||||
|
@ -1,14 +1,24 @@
|
||||
import pytest
|
||||
from pytest_httpbin.plugin import httpbin_ca_bundle
|
||||
from pytest_httpbin import certs
|
||||
|
||||
|
||||
# Make httpbin's CA trusted by default
|
||||
pytest.fixture(autouse=True)(httpbin_ca_bundle)
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def httpbin_add_ca_bundle(monkeypatch):
|
||||
"""
|
||||
Make pytest-httpbin's CA trusted by default.
|
||||
|
||||
(Same as `httpbin_ca_bundle`, just auto-used.).
|
||||
|
||||
"""
|
||||
monkeypatch.setenv('REQUESTS_CA_BUNDLE', certs.where())
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def httpbin_secure_untrusted(monkeypatch, httpbin_secure):
|
||||
"""Like the `httpbin_secure` fixture, but without the
|
||||
make-CA-trusted-by-default"""
|
||||
"""
|
||||
Like the `httpbin_secure` fixture, but without the
|
||||
make-CA-trusted-by-default.
|
||||
|
||||
"""
|
||||
monkeypatch.delenv('REQUESTS_CA_BUNDLE')
|
||||
return httpbin_secure
|
||||
|
@ -2,7 +2,7 @@
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from utils import http, add_auth, HTTP_OK, TestEnvironment
|
||||
from utils import http, add_auth, HTTP_OK, MockEnvironment
|
||||
import httpie.input
|
||||
import httpie.cli
|
||||
|
||||
@ -55,10 +55,21 @@ def test_credentials_in_url_auth_flag_has_priority(httpbin_both):
|
||||
])
|
||||
def test_only_username_in_url(url):
|
||||
"""
|
||||
https://github.com/jkbrzt/httpie/issues/242
|
||||
https://github.com/jakubroztocil/httpie/issues/242
|
||||
|
||||
"""
|
||||
args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
|
||||
args = httpie.cli.parser.parse_args(args=[url], env=MockEnvironment())
|
||||
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,15 +1,16 @@
|
||||
"""Tests for dealing with binary request and response data."""
|
||||
from httpie.compat import urlopen
|
||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||
from utils import TestEnvironment, http
|
||||
import requests
|
||||
|
||||
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
|
||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||
from utils import MockEnvironment, http
|
||||
|
||||
|
||||
class TestBinaryRequestData:
|
||||
|
||||
def test_binary_stdin(self, httpbin):
|
||||
with open(BIN_FILE_PATH, 'rb') as stdin:
|
||||
env = TestEnvironment(
|
||||
env = MockEnvironment(
|
||||
stdin=stdin,
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=False
|
||||
@ -18,38 +19,32 @@ class TestBinaryRequestData:
|
||||
assert r == BIN_FILE_CONTENT
|
||||
|
||||
def test_binary_file_path(self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--print=B', 'POST', httpbin.url + '/post',
|
||||
'@' + BIN_FILE_PATH_ARG, env=env, )
|
||||
assert r == BIN_FILE_CONTENT
|
||||
|
||||
def test_binary_file_form(self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--print=B', '--form', 'POST', httpbin.url + '/post',
|
||||
'test@' + BIN_FILE_PATH_ARG, env=env)
|
||||
assert bytes(BIN_FILE_CONTENT) in bytes(r)
|
||||
|
||||
|
||||
class TestBinaryResponseData:
|
||||
url = 'http://www.google.com/favicon.ico'
|
||||
|
||||
@property
|
||||
def bindata(self):
|
||||
if not hasattr(self, '_bindata'):
|
||||
self._bindata = urlopen(self.url).read()
|
||||
return self._bindata
|
||||
|
||||
def test_binary_suppresses_when_terminal(self):
|
||||
r = http('GET', self.url)
|
||||
def test_binary_suppresses_when_terminal(self, httpbin):
|
||||
r = http('GET', httpbin + '/bytes/1024')
|
||||
assert BINARY_SUPPRESSED_NOTICE.decode() in r
|
||||
|
||||
def test_binary_suppresses_when_not_terminal_but_pretty(self):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--pretty=all', 'GET', self.url,
|
||||
env=env)
|
||||
def test_binary_suppresses_when_not_terminal_but_pretty(self, httpbin):
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--pretty=all', 'GET', httpbin + '/bytes/1024', env=env)
|
||||
assert BINARY_SUPPRESSED_NOTICE.decode() in r
|
||||
|
||||
def test_binary_included_and_correct_when_suitable(self):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('GET', self.url, env=env)
|
||||
assert r == self.bindata
|
||||
def test_binary_included_and_correct_when_suitable(self, httpbin):
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
url = httpbin + '/bytes/1024?seed=1'
|
||||
r = http('GET', url, env=env)
|
||||
expected = requests.get(url).content
|
||||
assert r == expected
|
||||
|
@ -10,7 +10,7 @@ from httpie import input
|
||||
from httpie.input import KeyValue, KeyValueArgType, DataDict
|
||||
from httpie import ExitStatus
|
||||
from httpie.cli import parser
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from fixtures import (
|
||||
FILE_PATH_ARG, JSON_FILE_PATH_ARG,
|
||||
JSON_FILE_CONTENT, FILE_CONTENT, FILE_PATH
|
||||
@ -49,9 +49,9 @@ class TestItemParsing:
|
||||
assert 'bar@baz' in items.files
|
||||
|
||||
@pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [
|
||||
('path=c:\windows', 'path', '=', 'c:\windows'),
|
||||
('path=c:\windows\\', 'path', '=', 'c:\windows\\'),
|
||||
('path\==c:\windows', 'path=', '=', 'c:\windows'),
|
||||
('path=c:\\windows', 'path', '=', 'c:\\windows'),
|
||||
('path=c:\\windows\\', 'path', '=', 'c:\\windows\\'),
|
||||
('path\\==c:\\windows', 'path=', '=', 'c:\\windows'),
|
||||
])
|
||||
def test_backslash_before_non_special_character_does_not_escape(
|
||||
self, string, key, sep, value):
|
||||
@ -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([
|
||||
@ -156,44 +161,44 @@ class TestQuerystring:
|
||||
|
||||
class TestLocalhostShorthand:
|
||||
def test_expand_localhost_shorthand(self):
|
||||
args = parser.parse_args(args=[':'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost'
|
||||
|
||||
def test_expand_localhost_shorthand_with_slash(self):
|
||||
args = parser.parse_args(args=[':/'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':/'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost/'
|
||||
|
||||
def test_expand_localhost_shorthand_with_port(self):
|
||||
args = parser.parse_args(args=[':3000'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':3000'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost:3000'
|
||||
|
||||
def test_expand_localhost_shorthand_with_path(self):
|
||||
args = parser.parse_args(args=[':/path'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':/path'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost/path'
|
||||
|
||||
def test_expand_localhost_shorthand_with_port_and_slash(self):
|
||||
args = parser.parse_args(args=[':3000/'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':3000/'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost:3000/'
|
||||
|
||||
def test_expand_localhost_shorthand_with_port_and_path(self):
|
||||
args = parser.parse_args(args=[':3000/path'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=[':3000/path'], env=MockEnvironment())
|
||||
assert args.url == 'http://localhost:3000/path'
|
||||
|
||||
def test_dont_expand_shorthand_ipv6_as_shorthand(self):
|
||||
args = parser.parse_args(args=['::1'], env=TestEnvironment())
|
||||
args = parser.parse_args(args=['::1'], env=MockEnvironment())
|
||||
assert args.url == 'http://::1'
|
||||
|
||||
def test_dont_expand_longer_ipv6_as_shorthand(self):
|
||||
args = parser.parse_args(
|
||||
args=['::ffff:c000:0280'],
|
||||
env=TestEnvironment()
|
||||
env=MockEnvironment()
|
||||
)
|
||||
assert args.url == 'http://::ffff:c000:0280'
|
||||
|
||||
def test_dont_expand_full_ipv6_as_shorthand(self):
|
||||
args = parser.parse_args(
|
||||
args=['0000:0000:0000:0000:0000:0000:0000:0001'],
|
||||
env=TestEnvironment()
|
||||
env=MockEnvironment()
|
||||
)
|
||||
assert args.url == 'http://0000:0000:0000:0000:0000:0000:0000:0001'
|
||||
|
||||
@ -210,7 +215,7 @@ class TestArgumentParser:
|
||||
self.parser.args.items = []
|
||||
self.parser.args.ignore_stdin = False
|
||||
|
||||
self.parser.env = TestEnvironment()
|
||||
self.parser.env = MockEnvironment()
|
||||
|
||||
self.parser._guess_method()
|
||||
|
||||
@ -224,7 +229,7 @@ class TestArgumentParser:
|
||||
self.parser.args.url = 'http://example.com/'
|
||||
self.parser.args.items = []
|
||||
self.parser.args.ignore_stdin = False
|
||||
self.parser.env = TestEnvironment()
|
||||
self.parser.env = MockEnvironment()
|
||||
|
||||
self.parser._guess_method()
|
||||
|
||||
@ -238,7 +243,7 @@ class TestArgumentParser:
|
||||
self.parser.args.url = 'data=field'
|
||||
self.parser.args.items = []
|
||||
self.parser.args.ignore_stdin = False
|
||||
self.parser.env = TestEnvironment()
|
||||
self.parser.env = MockEnvironment()
|
||||
self.parser._guess_method()
|
||||
|
||||
assert self.parser.args.method == 'POST'
|
||||
@ -257,7 +262,7 @@ class TestArgumentParser:
|
||||
self.parser.args.items = []
|
||||
self.parser.args.ignore_stdin = False
|
||||
|
||||
self.parser.env = TestEnvironment()
|
||||
self.parser.env = MockEnvironment()
|
||||
|
||||
self.parser._guess_method()
|
||||
|
||||
@ -280,7 +285,7 @@ class TestArgumentParser:
|
||||
]
|
||||
self.parser.args.ignore_stdin = False
|
||||
|
||||
self.parser.env = TestEnvironment()
|
||||
self.parser.env = MockEnvironment()
|
||||
|
||||
self.parser._guess_method()
|
||||
|
||||
@ -309,7 +314,7 @@ class TestIgnoreStdin:
|
||||
|
||||
def test_ignore_stdin(self, httpbin):
|
||||
with open(FILE_PATH) as f:
|
||||
env = TestEnvironment(stdin=f, stdin_isatty=False)
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get',
|
||||
env=env)
|
||||
assert HTTP_OK in r
|
||||
@ -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')
|
||||
|
@ -1,8 +1,10 @@
|
||||
from utils import TestEnvironment, http
|
||||
from httpie import __version__
|
||||
from utils import MockEnvironment, http
|
||||
from httpie.context import Environment
|
||||
|
||||
|
||||
def test_default_options(httpbin):
|
||||
env = TestEnvironment()
|
||||
env = MockEnvironment()
|
||||
env.config['default_options'] = ['--form']
|
||||
env.config.save()
|
||||
r = http(httpbin.url + '/post', 'foo=bar', env=env)
|
||||
@ -10,7 +12,7 @@ def test_default_options(httpbin):
|
||||
|
||||
|
||||
def test_default_options_overwrite(httpbin):
|
||||
env = TestEnvironment()
|
||||
env = MockEnvironment()
|
||||
env.config['default_options'] = ['--form']
|
||||
env.config.save()
|
||||
r = http('--json', httpbin.url + '/post', 'foo=bar', env=env)
|
||||
@ -18,7 +20,7 @@ def test_default_options_overwrite(httpbin):
|
||||
|
||||
|
||||
def test_migrate_implicit_content_type():
|
||||
config = TestEnvironment().config
|
||||
config = MockEnvironment().config
|
||||
|
||||
config['implicit_content_type'] = 'json'
|
||||
config.save()
|
||||
@ -31,3 +33,8 @@ def test_migrate_implicit_content_type():
|
||||
config.load()
|
||||
assert 'implicit_content_type' not in config
|
||||
assert config['default_options'] == ['--form']
|
||||
|
||||
|
||||
def test_current_version():
|
||||
version = Environment().config['__meta__']['httpie']
|
||||
assert version == __version__
|
||||
|
@ -2,10 +2,26 @@
|
||||
Tests for the provided defaults regarding HTTP method, and --json vs. --form.
|
||||
|
||||
"""
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from httpie.client import JSON_ACCEPT
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH
|
||||
|
||||
|
||||
def test_default_headers_case_insensitive(httpbin):
|
||||
"""
|
||||
<https://github.com/jakubroztocil/httpie/issues/644>
|
||||
"""
|
||||
r = http(
|
||||
'--debug',
|
||||
'--print=H',
|
||||
httpbin.url + '/post',
|
||||
'CONTENT-TYPE:application/json-patch+json',
|
||||
'a=b',
|
||||
)
|
||||
assert 'CONTENT-TYPE: application/json-patch+json' in r
|
||||
assert 'Content-Type' not in r
|
||||
|
||||
|
||||
class TestImplicitHTTPMethod:
|
||||
def test_implicit_GET(self, httpbin):
|
||||
r = http(httpbin.url + '/get')
|
||||
@ -28,7 +44,7 @@ class TestImplicitHTTPMethod:
|
||||
|
||||
def test_implicit_POST_stdin(self, httpbin):
|
||||
with open(FILE_PATH) as f:
|
||||
env = TestEnvironment(stdin_isatty=False, stdin=f)
|
||||
env = MockEnvironment(stdin_isatty=False, stdin=f)
|
||||
r = http('--form', httpbin.url + '/post', env=env)
|
||||
assert HTTP_OK in r
|
||||
|
||||
@ -42,7 +58,7 @@ class TestAutoContentTypeAndAcceptHeaders:
|
||||
"""
|
||||
|
||||
def test_GET_no_data_no_auto_headers(self, httpbin):
|
||||
# https://github.com/jkbrzt/httpie/issues/62
|
||||
# https://github.com/jakubroztocil/httpie/issues/62
|
||||
r = http('GET', httpbin.url + '/headers')
|
||||
assert HTTP_OK in r
|
||||
assert r.json['headers']['Accept'] == '*/*'
|
||||
@ -58,22 +74,22 @@ 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
|
||||
# https://github.com/jakubroztocil/httpie/issues/137
|
||||
assert 'application/json' in r.json['headers']['Content-Type']
|
||||
|
||||
def test_GET_explicit_JSON_explicit_headers(self, httpbin):
|
||||
@ -96,11 +112,11 @@ class TestAutoContentTypeAndAcceptHeaders:
|
||||
assert '"Content-Type": "application/xml"' in r
|
||||
|
||||
def test_print_only_body_when_stdout_redirected_by_default(self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('GET', httpbin.url + '/get', env=env)
|
||||
assert 'HTTP/' not in r
|
||||
|
||||
def test_print_overridable_when_stdout_redirected(self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--print=h', 'GET', httpbin.url + '/get', env=env)
|
||||
assert HTTP_OK in r
|
||||
|
@ -10,7 +10,7 @@ from httpie.downloads import (
|
||||
parse_content_range, filename_from_content_disposition, filename_from_url,
|
||||
get_unique_filename, ContentRangeError, Downloader,
|
||||
)
|
||||
from utils import http, TestEnvironment
|
||||
from utils import http, MockEnvironment
|
||||
|
||||
|
||||
class Response(object):
|
||||
@ -123,7 +123,7 @@ class TestDownloads:
|
||||
def test_actual_download(self, httpbin_both, httpbin):
|
||||
robots_txt = '/robots.txt'
|
||||
body = urlopen(httpbin + robots_txt).read().decode()
|
||||
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
||||
r = http('--download', httpbin_both.url + robots_txt, env=env)
|
||||
assert 'Downloading' in r.stderr
|
||||
assert '[K' in r.stderr
|
||||
|
@ -1,30 +1,46 @@
|
||||
import mock
|
||||
|
||||
from httpie import ExitStatus
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from utils import MockEnvironment, 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
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def test_error_response_exits_0_without_check_status(httpbin):
|
||||
r = http('GET', httpbin.url + '/status/500')
|
||||
assert '500 INTERNAL SERVER ERRO' in r
|
||||
assert r.exit_status == ExitStatus.OK
|
||||
assert '500 INTERNAL SERVER ERROR' in r
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
assert not r.stderr
|
||||
|
||||
|
||||
def test_timeout_exit_status(httpbin):
|
||||
|
||||
r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.02',
|
||||
r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.5',
|
||||
error_exit_ok=True)
|
||||
assert r.exit_status == ExitStatus.ERROR_TIMEOUT
|
||||
|
||||
|
||||
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(
|
||||
httpbin):
|
||||
env = TestEnvironment(stdout_isatty=False)
|
||||
env = MockEnvironment(stdout_isatty=False)
|
||||
r = http('--check-status', '--headers',
|
||||
'GET', httpbin.url + '/status/301',
|
||||
env=env, error_exit_ok=True)
|
||||
@ -39,7 +55,7 @@ def test_3xx_check_status_redirects_allowed_exits_0(httpbin):
|
||||
error_exit_ok=True)
|
||||
# The redirect will be followed so 200 is expected.
|
||||
assert HTTP_OK in r
|
||||
assert r.exit_status == ExitStatus.OK
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def test_4xx_check_status_exits_4(httpbin):
|
||||
|
@ -1,27 +1,28 @@
|
||||
"""High-level tests."""
|
||||
import pytest
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
|
||||
from httpie.input import ParseError
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH, FILE_CONTENT
|
||||
|
||||
import httpie
|
||||
from httpie.compat import is_py26
|
||||
|
||||
|
||||
def test_debug():
|
||||
r = http('--debug')
|
||||
assert r.exit_status == httpie.ExitStatus.OK
|
||||
assert r.exit_status == httpie.ExitStatus.SUCCESS
|
||||
assert 'HTTPie %s' % httpie.__version__ in r.stderr
|
||||
|
||||
|
||||
def test_help():
|
||||
r = http('--help', error_exit_ok=True)
|
||||
assert r.exit_status == httpie.ExitStatus.OK
|
||||
assert 'https://github.com/jkbrzt/httpie/issues' in r
|
||||
assert r.exit_status == httpie.ExitStatus.SUCCESS
|
||||
assert 'https://github.com/jakubroztocil/httpie/issues' in r
|
||||
|
||||
|
||||
def test_version():
|
||||
r = http('--version', error_exit_ok=True)
|
||||
assert r.exit_status == httpie.ExitStatus.OK
|
||||
assert r.exit_status == httpie.ExitStatus.SUCCESS
|
||||
# FIXME: py3 has version in stdout, py2 in stderr
|
||||
assert httpie.__version__ == r.stderr.strip() + r.strip()
|
||||
|
||||
@ -62,7 +63,7 @@ def test_POST_form_multiple_values(httpbin_both):
|
||||
|
||||
def test_POST_stdin(httpbin_both):
|
||||
with open(FILE_PATH) as f:
|
||||
env = TestEnvironment(stdin=f, stdin_isatty=False)
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--form', 'POST', httpbin_both + '/post', env=env)
|
||||
assert HTTP_OK in r
|
||||
assert FILE_CONTENT in r
|
||||
@ -75,10 +76,36 @@ def test_headers(httpbin_both):
|
||||
assert '"Foo": "bar"' in r
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
is_py26,
|
||||
reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only'
|
||||
)
|
||||
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')
|
||||
|
||||
|
||||
def test_json_input_preserve_order(httpbin_both):
|
||||
r = http('PATCH', httpbin_both + '/patch',
|
||||
'order:={"map":{"1":"first","2":"second"}}')
|
||||
|
@ -3,7 +3,7 @@ from tempfile import gettempdir
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF
|
||||
from utils import MockEnvironment, http, HTTP_OK, COLOR, CRLF
|
||||
from httpie import ExitStatus
|
||||
from httpie.compat import urlopen
|
||||
from httpie.output.formatters.colors import get_lexer
|
||||
@ -15,7 +15,7 @@ def test_output_option(httpbin, stdout_isatty):
|
||||
url = httpbin + '/robots.txt'
|
||||
|
||||
r = http('--output', output_filename, url,
|
||||
env=TestEnvironment(stdout_isatty=stdout_isatty))
|
||||
env=MockEnvironment(stdout_isatty=stdout_isatty))
|
||||
assert r == ''
|
||||
|
||||
expected_body = urlopen(url).read().decode()
|
||||
@ -33,7 +33,7 @@ class TestVerboseFlag:
|
||||
assert r.count('__test__') == 2
|
||||
|
||||
def test_verbose_form(self, httpbin):
|
||||
# https://github.com/jkbrzt/httpie/issues/53
|
||||
# https://github.com/jakubroztocil/httpie/issues/53
|
||||
r = http('--verbose', '--form', 'POST', httpbin.url + '/post',
|
||||
'A=B', 'C=D')
|
||||
assert HTTP_OK in r
|
||||
@ -86,7 +86,7 @@ class TestPrettyOptions:
|
||||
"""Test the --pretty flag handling."""
|
||||
|
||||
def test_pretty_enabled_by_default(self, httpbin):
|
||||
env = TestEnvironment(colors=256)
|
||||
env = MockEnvironment(colors=256)
|
||||
r = http('GET', httpbin.url + '/get', env=env)
|
||||
assert COLOR in r
|
||||
|
||||
@ -95,7 +95,7 @@ class TestPrettyOptions:
|
||||
assert COLOR not in r
|
||||
|
||||
def test_force_pretty(self, httpbin):
|
||||
env = TestEnvironment(stdout_isatty=False, colors=256)
|
||||
env = MockEnvironment(stdout_isatty=False, colors=256)
|
||||
r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, )
|
||||
assert COLOR in r
|
||||
|
||||
@ -108,13 +108,13 @@ class TestPrettyOptions:
|
||||
match any lexer.
|
||||
|
||||
"""
|
||||
env = TestEnvironment(colors=256)
|
||||
env = MockEnvironment(colors=256)
|
||||
r = http('--print=B', '--pretty=all', httpbin.url + '/post',
|
||||
'Content-Type:text/foo+json', 'a=b', env=env)
|
||||
assert COLOR in r
|
||||
|
||||
def test_colors_option(self, httpbin):
|
||||
env = TestEnvironment(colors=256)
|
||||
env = MockEnvironment(colors=256)
|
||||
r = http('--print=B', '--pretty=colors',
|
||||
'GET', httpbin.url + '/get', 'a=b',
|
||||
env=env)
|
||||
@ -123,7 +123,7 @@ class TestPrettyOptions:
|
||||
assert COLOR in r
|
||||
|
||||
def test_format_option(self, httpbin):
|
||||
env = TestEnvironment(colors=256)
|
||||
env = MockEnvironment(colors=256)
|
||||
r = http('--print=B', '--pretty=format',
|
||||
'GET', httpbin.url + '/get', 'a=b',
|
||||
env=env)
|
||||
@ -161,7 +161,7 @@ class TestLineEndings:
|
||||
|
||||
def test_CRLF_formatted_response(self, httpbin):
|
||||
r = http('--pretty=format', 'GET', httpbin.url + '/get')
|
||||
assert r.exit_status == ExitStatus.OK
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
self._validate_crlf(r)
|
||||
|
||||
def test_CRLF_ugly_request(self, httpbin):
|
||||
|
@ -7,7 +7,7 @@ from httpie.compat import is_windows
|
||||
|
||||
def test_Host_header_overwrite(httpbin):
|
||||
"""
|
||||
https://github.com/jkbrzt/httpie/issues/235
|
||||
https://github.com/jakubroztocil/httpie/issues/235
|
||||
|
||||
"""
|
||||
host = 'httpbin.org'
|
||||
@ -21,7 +21,7 @@ def test_Host_header_overwrite(httpbin):
|
||||
@pytest.mark.skipif(is_windows, reason='Unix-only')
|
||||
def test_output_devnull(httpbin):
|
||||
"""
|
||||
https://github.com/jkbrzt/httpie/issues/252
|
||||
https://github.com/jakubroztocil/httpie/issues/252
|
||||
|
||||
"""
|
||||
http('--output=/dev/null', httpbin + '/get')
|
||||
|
@ -7,7 +7,7 @@ from tempfile import gettempdir
|
||||
import pytest
|
||||
|
||||
from httpie.plugins.builtin import HTTPBasicAuth
|
||||
from utils import TestEnvironment, mk_config_dir, http, HTTP_OK
|
||||
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK
|
||||
from fixtures import UNICODE
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ class SessionTestBase(object):
|
||||
for session files being reused.
|
||||
|
||||
"""
|
||||
return TestEnvironment(config_dir=self.config_dir)
|
||||
return MockEnvironment(config_dir=self.config_dir)
|
||||
|
||||
|
||||
class TestSessionFlow(SessionTestBase):
|
||||
@ -81,8 +81,8 @@ class TestSessionFlow(SessionTestBase):
|
||||
assert HTTP_OK in r4
|
||||
assert r4.json['headers']['Hello'] == 'World2'
|
||||
assert r4.json['headers']['Cookie'] == 'hello=world2'
|
||||
assert (r2.json['headers']['Authorization'] !=
|
||||
r4.json['headers']['Authorization'])
|
||||
assert (r2.json['headers']['Authorization']
|
||||
!= r4.json['headers']['Authorization'])
|
||||
|
||||
def test_session_read_only(self, httpbin):
|
||||
self.start_session(httpbin)
|
||||
@ -143,7 +143,7 @@ class TestSession(SessionTestBase):
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info >= (3,),
|
||||
reason="This test fails intermittently on Python 3 - "
|
||||
"see https://github.com/jkbrzt/httpie/issues/282")
|
||||
"see https://github.com/jakubroztocil/httpie/issues/282")
|
||||
def test_session_unicode(self, httpbin):
|
||||
self.start_session(httpbin)
|
||||
|
||||
@ -157,14 +157,14 @@ class TestSession(SessionTestBase):
|
||||
assert HTTP_OK in r2
|
||||
|
||||
# FIXME: Authorization *sometimes* is not present on Python3
|
||||
assert (r2.json['headers']['Authorization'] ==
|
||||
HTTPBasicAuth.make_header(u'test', UNICODE))
|
||||
assert (r2.json['headers']['Authorization']
|
||||
== HTTPBasicAuth.make_header(u'test', UNICODE))
|
||||
# httpbin doesn't interpret utf8 headers
|
||||
assert UNICODE in r2
|
||||
|
||||
def test_session_default_header_value_overwritten(self, httpbin):
|
||||
self.start_session(httpbin)
|
||||
# https://github.com/jkbrzt/httpie/issues/180
|
||||
# https://github.com/jakubroztocil/httpie/issues/180
|
||||
r1 = http('--session=test',
|
||||
httpbin.url + '/headers', 'User-Agent:custom',
|
||||
env=self.env())
|
||||
@ -176,7 +176,7 @@ class TestSession(SessionTestBase):
|
||||
assert r2.json['headers']['User-Agent'] == 'custom'
|
||||
|
||||
def test_download_in_session(self, httpbin):
|
||||
# https://github.com/jkbrzt/httpie/issues/412
|
||||
# https://github.com/jakubroztocil/httpie/issues/412
|
||||
self.start_session(httpbin)
|
||||
cwd = os.getcwd()
|
||||
os.chdir(gettempdir())
|
||||
|
@ -2,17 +2,31 @@ import os
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
from requests.exceptions import SSLError
|
||||
import requests.exceptions
|
||||
|
||||
from httpie import ExitStatus
|
||||
from httpie.input import SSL_VERSION_ARG_MAPPING
|
||||
from utils import http, HTTP_OK, TESTS_ROOT
|
||||
from utils import HTTP_OK, TESTS_ROOT, http
|
||||
|
||||
|
||||
try:
|
||||
# Handle OpenSSL errors, if installed.
|
||||
# See <https://github.com/jakubroztocil/httpie/issues/729>
|
||||
# noinspection PyUnresolvedReferences
|
||||
import OpenSSL.SSL
|
||||
ssl_errors = (
|
||||
requests.exceptions.SSLError,
|
||||
OpenSSL.SSL.Error,
|
||||
)
|
||||
except ImportError:
|
||||
ssl_errors = (
|
||||
requests.exceptions.SSLError,
|
||||
)
|
||||
|
||||
|
||||
CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
|
||||
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
|
||||
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
|
||||
|
||||
# FIXME:
|
||||
# We test against a local httpbin instance which uses a self-signed cert.
|
||||
# Requests without --verify=<CA_BUNDLE> will fail with a verification error.
|
||||
@ -28,7 +42,7 @@ def test_ssl_version(httpbin_secure, ssl_version):
|
||||
httpbin_secure + '/get'
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
except SSLError as e:
|
||||
except ssl_errors as e:
|
||||
if ssl_version == 'ssl3':
|
||||
# pytest-httpbin doesn't support ssl3
|
||||
assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e)
|
||||
@ -57,12 +71,12 @@ class TestClientCert:
|
||||
assert 'No such file or directory' in r.stderr
|
||||
|
||||
def test_cert_file_invalid(self, httpbin_secure):
|
||||
with pytest.raises(SSLError):
|
||||
with pytest.raises(ssl_errors):
|
||||
http(httpbin_secure + '/get',
|
||||
'--cert', __file__)
|
||||
|
||||
def test_cert_ok_but_missing_key(self, httpbin_secure):
|
||||
with pytest.raises(SSLError):
|
||||
with pytest.raises(ssl_errors):
|
||||
http(httpbin_secure + '/get',
|
||||
'--cert', CLIENT_CERT)
|
||||
|
||||
@ -73,21 +87,29 @@ class TestServerCert:
|
||||
r = http(httpbin_secure.url + '/get', '--verify=no')
|
||||
assert HTTP_OK in r
|
||||
|
||||
@pytest.mark.parametrize('verify_value', ['false', 'fALse'])
|
||||
def test_verify_false_OK(self, httpbin_secure, verify_value):
|
||||
r = http(httpbin_secure.url + '/get', '--verify', verify_value)
|
||||
assert HTTP_OK in r
|
||||
|
||||
def test_verify_custom_ca_bundle_path(
|
||||
self, httpbin_secure_untrusted):
|
||||
self, httpbin_secure_untrusted
|
||||
):
|
||||
r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE)
|
||||
assert HTTP_OK in r
|
||||
|
||||
def test_self_signed_server_cert_by_default_raises_ssl_error(
|
||||
self,
|
||||
httpbin_secure_untrusted):
|
||||
with pytest.raises(SSLError):
|
||||
self,
|
||||
httpbin_secure_untrusted
|
||||
):
|
||||
with pytest.raises(ssl_errors):
|
||||
http(httpbin_secure_untrusted.url + '/get')
|
||||
|
||||
def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure):
|
||||
with pytest.raises(SSLError):
|
||||
# since 2.14.0 requests raises IOError
|
||||
with pytest.raises(ssl_errors + (IOError,)):
|
||||
http(httpbin_secure.url + '/get', '--verify', '/__not_found__')
|
||||
|
||||
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
|
||||
with pytest.raises(SSLError):
|
||||
with pytest.raises(ssl_errors):
|
||||
http(httpbin_secure.url + '/get', '--verify', __file__)
|
||||
|
@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
|
||||
from utils import http, TestEnvironment
|
||||
from utils import http, MockEnvironment
|
||||
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
|
||||
def test_pretty_redirected_stream(httpbin):
|
||||
"""Test that --stream works with prettified redirected output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = TestEnvironment(colors=256, stdin=f,
|
||||
env = MockEnvironment(colors=256, stdin=f,
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=False)
|
||||
r = http('--verbose', '--pretty=all', '--stream', 'GET',
|
||||
@ -26,7 +26,7 @@ def test_encoded_stream(httpbin):
|
||||
"""Test that --stream works with non-prettified
|
||||
redirected terminal output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = TestEnvironment(stdin=f, stdin_isatty=False)
|
||||
env = MockEnvironment(stdin=f, stdin_isatty=False)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
httpbin.url + '/get', env=env)
|
||||
assert BINARY_SUPPRESSED_NOTICE.decode() in r
|
||||
@ -36,7 +36,7 @@ def test_redirected_stream(httpbin):
|
||||
"""Test that --stream works with non-prettified
|
||||
redirected terminal output."""
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
env = TestEnvironment(stdout_isatty=False,
|
||||
env = MockEnvironment(stdout_isatty=False,
|
||||
stdin_isatty=False,
|
||||
stdin=f)
|
||||
r = http('--pretty=none', '--stream', '--verbose', 'GET',
|
||||
|
@ -3,7 +3,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from httpie.input import ParseError
|
||||
from utils import TestEnvironment, http, HTTP_OK
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
|
||||
|
||||
|
||||
@ -62,14 +62,14 @@ class TestRequestBodyFromFilePath:
|
||||
|
||||
def test_request_body_from_file_by_path_no_field_name_allowed(
|
||||
self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=True)
|
||||
env = MockEnvironment(stdin_isatty=True)
|
||||
r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG,
|
||||
env=env, error_exit_ok=True)
|
||||
assert 'perhaps you meant --form?' in r.stderr
|
||||
|
||||
def test_request_body_from_file_by_path_no_data_items_allowed(
|
||||
self, httpbin):
|
||||
env = TestEnvironment(stdin_isatty=False)
|
||||
env = MockEnvironment(stdin_isatty=False)
|
||||
r = http('POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'foo=bar',
|
||||
env=env, error_exit_ok=True)
|
||||
assert 'cannot be mixed' in r.stderr
|
||||
|
@ -4,7 +4,7 @@ import tempfile
|
||||
import pytest
|
||||
from httpie.context import Environment
|
||||
|
||||
from utils import TestEnvironment, http
|
||||
from utils import MockEnvironment, http
|
||||
from httpie.compat import is_windows
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class TestWindowsOnly:
|
||||
|
||||
class TestFakeWindows:
|
||||
def test_output_file_pretty_not_allowed_on_windows(self, httpbin):
|
||||
env = TestEnvironment(is_windows=True)
|
||||
env = MockEnvironment(is_windows=True)
|
||||
output_file = os.path.join(
|
||||
tempfile.gettempdir(),
|
||||
self.test_output_file_pretty_not_allowed_on_windows.__name__
|
||||
|
@ -33,7 +33,7 @@ def add_auth(url, auth):
|
||||
return proto + '://' + auth + '@' + rest
|
||||
|
||||
|
||||
class TestEnvironment(Environment):
|
||||
class MockEnvironment(Environment):
|
||||
"""Environment subclass with reasonable defaults for testing."""
|
||||
colors = 0
|
||||
stdin_isatty = True,
|
||||
@ -51,7 +51,7 @@ class TestEnvironment(Environment):
|
||||
mode='w+t',
|
||||
prefix='httpie_stderr'
|
||||
)
|
||||
super(TestEnvironment, self).__init__(**kwargs)
|
||||
super(MockEnvironment, self).__init__(**kwargs)
|
||||
self._delete_config_dir = False
|
||||
|
||||
@property
|
||||
@ -59,7 +59,7 @@ class TestEnvironment(Environment):
|
||||
if not self.config_dir.startswith(tempfile.gettempdir()):
|
||||
self.config_dir = mk_config_dir()
|
||||
self._delete_config_dir = True
|
||||
return super(TestEnvironment, self).config
|
||||
return super(MockEnvironment, self).config
|
||||
|
||||
def cleanup(self):
|
||||
if self._delete_config_dir:
|
||||
@ -119,8 +119,8 @@ class StrCLIResponse(str, BaseCLIResponse):
|
||||
elif self.strip().startswith('{'):
|
||||
# Looks like JSON body.
|
||||
self._json = json.loads(self)
|
||||
elif (self.count('Content-Type:') == 1 and
|
||||
'application/json' in self):
|
||||
elif (self.count('Content-Type:') == 1
|
||||
and 'application/json' in self):
|
||||
# Looks like a whole JSON HTTP message,
|
||||
# try to extract its body.
|
||||
try:
|
||||
@ -183,7 +183,7 @@ def http(*args, **kwargs):
|
||||
error_exit_ok = kwargs.pop('error_exit_ok', False)
|
||||
env = kwargs.get('env')
|
||||
if not env:
|
||||
env = kwargs['env'] = TestEnvironment()
|
||||
env = kwargs['env'] = MockEnvironment()
|
||||
|
||||
stdout = env.stdout
|
||||
stderr = env.stderr
|
||||
@ -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')
|
||||
@ -219,7 +219,7 @@ def http(*args, **kwargs):
|
||||
sys.stderr.write(stderr.read())
|
||||
raise
|
||||
else:
|
||||
if not error_exit_ok and exit_status != ExitStatus.OK:
|
||||
if not error_exit_ok and exit_status != ExitStatus.SUCCESS:
|
||||
dump_stderr()
|
||||
raise ExitStatusError(
|
||||
'httpie.core.main() unexpectedly returned'
|
||||
@ -243,7 +243,7 @@ def http(*args, **kwargs):
|
||||
r.stderr = stderr.read()
|
||||
r.exit_status = exit_status
|
||||
|
||||
if r.exit_status != ExitStatus.OK:
|
||||
if r.exit_status != ExitStatus.SUCCESS:
|
||||
sys.stderr.write(r.stderr)
|
||||
|
||||
return r
|
||||
|
6
tox.ini
6
tox.ini
@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
[tox]
|
||||
envlist = py26, py27, py35, pypy
|
||||
# pypy3 currently fails because of a Flask issue
|
||||
envlist = py27, py37, pypy
|
||||
|
||||
|
||||
[testenv]
|
||||
@ -20,3 +21,6 @@ commands =
|
||||
--verbose \
|
||||
--doctest-modules \
|
||||
{posargs:./httpie ./tests}
|
||||
|
||||
[testenv:py27-osx-builtin]
|
||||
basepython = /usr/bin/python2.7
|
||||
|
Reference in New Issue
Block a user