Compare commits

...

150 Commits
0.9.3 ... 0.9.6

Author SHA1 Message Date
Jakub Roztocil
8e96238323 v0.9.6 2016-08-13 23:01:05 +02:00
Jakub Roztocil
8a9206eceb Fixed Makefile 2016-08-13 22:57:44 +02:00
Jakub Roztocil
8ac3c5961c Upgrade Pygments version 2016-08-13 22:57:33 +02:00
Jakub Roztocil
487c7a9221 v0.9.5 2016-08-13 22:51:42 +02:00
Jakub Roztocil
6d65668355 Strip request header values 2016-08-13 22:40:01 +02:00
Jakub Roztočil
3e5115e4a2 Merge pull request #501 from ii-v/master
Fixed spelling mistake in the AUTHORS.rst file
2016-08-11 08:37:41 +02:00
ii-v
2b8b572f22 Merge pull request #1 from ii-v/ii-v-patch-1
Fixed spelling mistake `GitHib` to `GitHub`
2016-08-11 01:44:04 +02:00
ii-v
af737fd338 Fixed spelling mistage GitHib to GitHub 2016-08-11 01:43:15 +02:00
Jakub Roztočil
ee375b6942 Merge pull request #493 from medecau/codestyle_environment
Codestyle environment
2016-07-29 23:17:00 +02:00
Pedro Rodrigues
becb63de9a useful info 2016-07-26 21:59:34 +01:00
Pedro Rodrigues
86c8abc485 force os to be linux (+1 squashed commit)
Squashed commits:
[444c56d] no vars for you (+1 squashed commit)
Squashed commits:
[c7d1bf9] added pycodestyle environment to travis config
2016-07-26 21:43:13 +01:00
Pedro Rodrigues
8f6bee9196 codestyle fixes 2016-07-19 17:23:40 +01:00
Pedro Rodrigues
9c2c058ae5 separate environment to test codestyle as proposed by @sigmavirus24 2016-07-19 17:23:18 +01:00
Jakub Roztocil
6238b59e72 Fix formatting 2016-07-08 15:05:43 +02:00
Jakub Roztocil
702c21aa91 Added related projects 2016-07-08 15:03:48 +02:00
Jakub Roztocil
aab5cd9da0 PEP8. clean-up 2016-07-04 20:30:55 +02:00
Jakub Roztocil
8c0f0b578c Clean-up 2016-07-02 18:44:02 +02:00
Jakub Roztocil
bb4881a873 Fixed README 2016-07-02 18:30:04 +02:00
Jakub Roztocil
3a1726b4ed Fixed README 2016-07-02 15:04:19 +02:00
Jakub Roztocil
e1fa57d228 Added -I as a shortcut for --ignore-stdin 2016-07-02 15:01:46 +02:00
Jakub Roztocil
bfc64bce21 Upgrade requests to 2.10.0 to enable optional SOCKS support
Closes #86
2016-07-02 14:58:34 +02:00
Jakub Roztocil
595dc51b2d Fish shell completion 2016-07-02 14:33:04 +02:00
Jakub Roztočil
83fa772247 Merge pull request #459 from dickeyxxx/fish-completion
added completions for fish shell
2016-07-02 14:31:06 +02:00
Jakub Roztocil
49a0fb6e0f More liberal default JSON Accept header
Closes #470
2016-07-02 14:18:36 +02:00
Jakub Roztocil
41e822ca2f Clean-up 2016-07-02 12:51:35 +02:00
Jakub Roztocil
1124d68946 Added --default-scheme <URL_SCHEME>
Closes #289
2016-07-02 12:47:02 +02:00
Jakub Roztočil
c3735d0422 Merge pull request #401 from lgarron/default-scheme
Add a --default-scheme argument.
2016-07-02 12:32:07 +02:00
Jakub Roztocil
364b91cbc4 Skip pypy3 tests on TravisCI 2016-07-02 12:03:52 +02:00
Jakub Roztocil
c8e06b55e1 Fix tests 2016-07-02 12:03:19 +02:00
Jakub Roztocil
5acbc904b7 Added the ability to unset headers
Closes #476
2016-07-02 11:50:30 +02:00
Jakub Roztocil
0c7c248dce Fix CHANGELOG 2016-07-02 11:17:38 +02:00
Jakub Roztocil
caf60cbc65 Typos 2016-07-02 11:11:06 +02:00
Jakub Roztocil
2b0e642842 Document preference for Python 3
Also mention that the Homebrew formula depends on Python 3 starting with HTTPie 0.9.4.
2016-07-02 11:07:46 +02:00
Jakub Roztocil
e25948f6a0 1.0.0-dev 2016-07-01 19:17:31 +02:00
Jakub Roztocil
b565b4628e v0.9.4 2016-07-01 19:02:34 +02:00
Jakub Roztocil
65081b2f12 Cleanup 2016-07-01 19:00:06 +02:00
Jakub Roztocil
963b2746f5 Be more liberal when detecting JSON in the formatter
Closes #485
2016-07-01 18:57:13 +02:00
Jakub Roztocil
098257c0be Rename --print-others to --history-print. 2016-07-01 18:49:27 +02:00
Jakub Roztočil
30eb0c2f26 Merge pull request #468 from Natim/master
Update readthedocs links.
2016-04-28 23:14:52 +08:00
Rémy HUBSCHER
9fbe745987 Update readthedocs links. 2016-04-28 12:28:20 +02:00
Jakub Roztočil
01a546eedd Merge pull request #463 from KyleAMathews/patch-1
Remove extra backtick.
2016-04-20 11:17:40 +08:00
Kyle Mathews
eba6b63c55 Remove extra backtick. 2016-04-18 15:11:04 -07:00
Jeff Dickey
ec245a1e80 added completions for fish shell 2016-04-06 11:28:03 -07:00
Jakub Roztocil
33eb9acd92 Updated README 2016-03-18 09:20:19 +08:00
Jakub Roztocil
293295cad6 Removed XML formatter
Closes #443
Closes #389
Closes #415
Closes #384
Closes #394
2016-03-18 09:16:39 +08:00
Jakub Roztocil
557911b606 Handle that os.pathconf is posix-only 2016-03-17 16:14:14 +08:00
Jakub Roztocil
5300b0b490 Fixed #451 - OSError: [Errno 36] File name too long 2016-03-17 15:58:01 +08:00
Jakub Roztocil
001bda1945 README 2016-03-17 15:00:50 +08:00
Jakub Roztocil
7c68d87c10 README 2016-03-10 14:27:33 +08:00
Jakub Roztocil
35a99fe04b Added test for -F shortcut 2016-03-09 21:58:34 +08:00
Jakub Roztocil
76e15b227c Added test_verbose_implies_all 2016-03-09 21:58:11 +08:00
Jakub Roztocil
8881ebf033 Changed the version icon label to include to word "stable" 2016-03-09 13:49:00 +08:00
Jakub Roztocil
25d1e8e418 Add Accept-Encoding: identity for --download
#423
2016-03-07 11:46:59 +08:00
Jakub Roztocil
7ce6eb148e Typo 2016-03-07 07:09:58 +08:00
Jakub Roztocil
6e1dbadff9 Replace --show-redirects with --all and add --print-others, -P
With --all, any intermediary requests/responses are shown (such as redirects
 or the initial unauthorized Digest auth request).

 The --print-others, -P option works like --print, -p, but only applies to
 intermediary requests/responses. The default behaviour is to inherit
 the value of -p.
2016-03-07 07:04:23 +08:00
Jakub Roztocil
a6ebc44a48 Run tests against both HTTP and HTTPS
Some of the tests now use the `httpbin_both` fixture from pytest-httpbin.
Also, made httpbin's CA trusted by default and added `httpbin_secure_untrusted`
fixture  to allow overriding that for particular tests.
2016-03-06 17:42:35 +08:00
Jakub Roztocil
5e03aeceb7 Make fruity default style one Windows
(again)
2016-03-06 08:33:40 +08:00
Jakub Roztocil
13ee9389aa Add link to contributors 2016-03-05 19:48:35 +08:00
Jakub Roztocil
bb49a1f979 Improved --debug output 2016-03-05 01:42:48 +08:00
Jakub Roztocil
4e574e6b8e Cleanup tests 2016-03-03 18:50:18 +08:00
Jakub Roztocil
529981af7a Fix CHANGELOG 2016-03-03 18:46:58 +08:00
Jakub Roztocil
6731cb881a README 2016-03-03 17:26:47 +08:00
Jakub Roztocil
f7d1b739e2 README 2016-03-03 17:24:46 +08:00
Jakub Roztocil
5bdf4a3bae Fixed test_rst_file_syntax error message 2016-03-03 17:22:12 +08:00
Jakub Roztocil
2d9414d34c Fixed README 2016-03-03 17:21:51 +08:00
Jakub Roztocil
20823c1702 Removed the "implicit_content_type" config option
If you used:

    "implicit_content_type": "form"

 You can achieve the the same result with:

     "default_options": ["--form"]

If you used:

    "implicit_content_type": "json"

 Then it's the default behaviour and it can be removed.

 In either case HTTPie will migrate your config file on the next invocation.
2016-03-03 17:14:39 +08:00
Jakub Roztocil
5dbd104c3b Nobody ain't got time for that 2016-03-03 17:09:34 +08:00
Jakub Roztocil
13a979ad11 Cleanup 2016-03-02 14:20:35 +08:00
Jakub Roztocil
4cfa143bfe Fixed coverage 2016-03-02 13:31:40 +08:00
Jakub Roztocil
d24f30d0af Cleanup 2016-03-02 13:31:23 +08:00
Jakub Roztocil
66e168b2af Improved failed test output 2016-03-02 13:16:41 +08:00
Jakub Roztocil
564670566c Fix coveralls integration 2016-03-02 12:25:19 +08:00
Jakub Roztocil
ecbbad816a Fix coveralls integration 2016-03-02 12:24:46 +08:00
Jakub Roztocil
0432694661 Changel 2016-03-02 12:24:24 +08:00
Jakub Roztocil
dc4da527db Added --ssl=<PROTOCOL_VERSION>
Closes #98
2016-03-02 12:12:05 +08:00
Jakub Roztocil
38e8ef14ec Run positive tests first
Trying to debug failing SSL tests on Travis - kevin1024/pytest-httpbin#32
2016-03-02 10:35:40 +08:00
Jakub Roztocil
c73dcaf63d CI 2016-03-02 10:08:20 +08:00
Jakub Roztocil
fb85509e91 CI 2016-03-02 09:58:50 +08:00
Jakub Roztocil
a2dca1e3bb CI 2016-03-02 09:44:39 +08:00
Jakub Roztocil
c2dae62af0 Appveyor: added Python 3.5 build 2016-03-02 09:37:58 +08:00
Jakub Roztocil
ae7008ee96 Appveyor 2016-03-02 09:33:43 +08:00
Jakub Roztocil
f6824f7ade Cleanup 2016-03-02 02:53:23 +08:00
Jakub Roztocil
7fd46e0b0d Cleanup 2016-03-02 01:02:11 +08:00
Jakub Roztocil
d4067fcb6d Added a short timeout for test requests 2016-03-02 00:31:00 +08:00
Jakub Roztocil
20f01709ea Mention URL escaping
Closes #311
2016-03-01 23:48:13 +08:00
Jakub Roztocil
56afd1adb9 Test suite cleanup 2016-03-01 23:22:50 +08:00
Jakub Roztocil
5e87a2d7e5 Cleanup 2016-03-01 23:13:45 +08:00
Jakub Roztocil
d30e28c2c7 Test suite improvements 2016-03-01 23:11:06 +08:00
Jakub Roztocil
0d2d24eac7 Copy 2016-03-01 21:37:26 +08:00
Jakub Roztocil
e2751e5fa3 Fixed args for Python 2.x 2016-03-01 21:28:10 +08:00
Jakub Roztocil
2a25d71aa4 Refactored main() into program() + main() 2016-03-01 21:10:54 +08:00
Jakub Roztocil
01ca7f0eb2 Ignore redirected stdout with --output, -o
This makes it easier to use HTTPie in cron jobs and scripts.

Closes #259
2016-03-01 20:24:50 +08:00
Jakub Roztocil
4f8d6c013b Fixed get_lexer() 2016-03-01 16:55:12 +08:00
Jakub Roztocil
e83e554ffb README 2016-03-01 16:50:30 +08:00
Jakub Roztocil
345f5a02a2 Fixed json absolute import 2016-03-01 16:39:50 +08:00
Jakub Roztocil
f96f0ef9ed JSON detection improvements 2016-03-01 16:22:54 +08:00
Jakub Roztocil
74e4d0b678 Added JSON detection when `--json, -j` is set
To correctly format JSON responses even when an incorrect ``Content-Type`` is returned.

Closes #92
Closes #349
Closes #368
2016-03-01 14:57:15 +08:00
Jakub Roztocil
0fc1f61f3d Fixed README 2016-03-01 00:45:54 +08:00
Jakub Roztocil
c50413a9c1 Added support section 2016-03-01 00:24:52 +08:00
Jakub Roztocil
9f8c452e7e Added gitter chat 2016-03-01 00:12:55 +08:00
Jakub Roztocil
776328c818 Added gitter chat 2016-03-01 00:08:07 +08:00
Jakub Roztocil
9312fabc01 Capitalization II 2016-02-29 22:03:08 +08:00
Jakub Roztocil
48ce934dfa Capitalization 2016-02-29 22:01:05 +08:00
Jakub Roztocil
3625bb6fa1 Updated travis badge title 2016-02-29 22:00:18 +08:00
Jakub Roztocil
a97f0d52f6 Travis 2016-02-29 17:20:50 +08:00
Jakub Roztocil
41b0286f37 Travis 2016-02-29 17:19:12 +08:00
Jakub Roztocil
fee54b04d8 Travis 2016-02-29 17:09:55 +08:00
Jakub Roztocil
73e0455896 Travis 2016-02-29 16:52:33 +08:00
Jakub Roztocil
3b217daddc Travis 2016-02-29 16:43:18 +08:00
Jakub Roztocil
e5e5d0ce6d Travis 2016-02-29 16:36:25 +08:00
Jakub Roztocil
f43e473de1 Travis 2016-02-29 16:35:09 +08:00
Jakub Roztocil
0a002ec554 Fix travis 2016-02-29 15:38:59 +08:00
Jakub Roztocil
576ee83d82 Fix travis 2016-02-29 15:35:56 +08:00
Jakub Roztocil
e42f7b8fc9 OSX build 2016-02-29 15:32:10 +08:00
Jakub Roztočil
b44e16ed0f Fix appveyor 2016-02-29 15:14:43 +08:00
Jakub Roztocil
ed08ab133e Refactoring 2016-02-29 15:00:17 +08:00
Jakub Roztocil
5408fb0fb9 Cleanup 2016-02-29 14:31:27 +08:00
Jakub Roztocil
e18b609ef7 Fixed --max-redirects 2016-02-29 14:21:25 +08:00
Jakub Roztocil
356e043651 Added --show-redirects and --max-redirects
Closes #157, #183, #188, #246
2016-02-29 14:12:09 +08:00
Jakub Roztocil
c6d4f6cdf6 Show redirects WIP 2016-02-29 14:07:08 +08:00
James Carr
dc1371d4d6 Implemented --max-redirects option
Added argument to argparse, changed client so that it uses a new
requests Session() with the number of redirects and a single test to
show the setting works.
2016-02-29 14:07:08 +08:00
Jakub Roztočil
e2235e56dc Update CHANGELOG.rst 2016-02-29 02:38:09 +08:00
Jakub Roztočil
763935b77f Update CHANGELOG.rst 2016-02-29 02:37:36 +08:00
Jakub Roztocil
6435532f72 Updated CHANGELOG 2016-02-28 21:37:49 +08:00
Jakub Roztocil
11a37067e7 Document -A as a short name for --auth-type 2016-02-28 21:01:21 +08:00
Jakub Roztočil
25f0156502 Merge pull request #432 from hangtwenty/master
Add `-A` as short arg for `--auth-type`
2016-02-28 20:58:23 +08:00
Jakub Roztocil
0f8d04b4df More robust mime type parsing
Closes #344
2016-02-28 20:12:16 +08:00
Jakub Roztocil
e385ed6a99 Fix README 2016-02-28 19:32:19 +08:00
Jakub Roztocil
01fdab55e9 Explain how to send fieldnames and headers starting with '-'
Closes #355
2016-02-28 19:31:43 +08:00
Jakub Roztocil
1127557742 Cleanup 2016-02-28 19:15:35 +08:00
Jakub Roztocil
5898879395 Fixed --download with --session
Closes #412
2016-02-28 19:14:10 +08:00
Jakub Roztocil
8c33e5e3d3 Parser => HTTPieArgumentParser 2016-02-28 19:01:54 +08:00
Jakub Roztocil
10da7b63a3 Mention MacPorts installation method
Closes #395
2016-02-28 16:54:33 +08:00
Jakub Roztocil
df193a373f Updated tarball URL 2016-02-28 16:20:19 +08:00
Jakub Roztocil
c2f8c36952 Updated download example URLs 2016-02-28 16:19:18 +08:00
Jakub Roztocil
56f498c153 Detect Content Type of file uploaded in multipart/form-data request
Closes #271 #285 #398

This adds filename-based detection. It's still not possible to specify the
content type manually, though.
2016-02-28 15:49:01 +08:00
Marcin Szewczyk
59e22b16b8 When possible, guess the content-type of the file being sent
Refined PR #285 by rasky to pass all tests
2016-02-28 15:47:43 +08:00
Jakub Roztočil
d32d6f29a9 Typo 2016-02-17 14:53:00 +08:00
Jakub Roztocil
274dddfb45 Changed the default color style back to solarized
Closes #440
2016-02-17 14:46:35 +08:00
Michael Floering
deb7b747cc Small fix for Python 2.6 compatibility.
Relates to #430 / #432.
2016-01-22 18:46:36 -06:00
Michael Floering
018e1f68de Merge remote-tracking branch 'upstream/master' 2016-01-22 18:40:53 -06:00
Michael Floering
ac69d4311b add -A as short arg for --auth-type
Addresses #430
comes with unit test
2016-01-22 18:37:30 -06:00
Jakub Roztočil
c75c4fa2a6 Merge pull request #429 from pra85/patch-1
Update license year range to 2016
2016-01-18 11:26:27 -03:00
Prayag Verma
a6a79e92e4 Update license year range to 2016 2016-01-18 11:20:24 +05:30
Jakub Roztočil
ea76542150 Added guardian/httpie-hmac-auth 2016-01-15 14:07:41 -03:00
Jakub Roztocil
c6690e0182 Makefile improvements 2016-01-02 14:33:48 -03:00
Jakub Roztocil
c82c9f0ae4 Makefile improvements 2016-01-02 14:28:46 -03:00
Jakub Roztocil
84b81c00ea Fixed tox.ini and improved tests and CONTRIBUTING.txt 2016-01-02 14:07:00 -03:00
Jakub Roztocil
34c6958dc8 1.0.0-dev 2016-01-01 19:38:21 -03:00
Lucas Garron
6259b5dd3b Add a --default-scheme argument. 2015-10-28 15:06:04 -07:00
55 changed files with 2020 additions and 1004 deletions

1
.coveragerc Normal file
View File

@@ -0,0 +1 @@
; needs to exist otherwise `$ coveralls` fails

18
.gitignore vendored
View File

@@ -1,11 +1,13 @@
dist .DS_Store
httpie.egg-info .idea/
build __pycache__/
dist/
httpie.egg-info/
build/
*.egg-info
.cache/
.tox
.coverage
*.pyc *.pyc
*.egg *.egg
.tox
README.html
.coverage
htmlcov htmlcov
.idea
.DS_Store

View File

@@ -1,20 +1,94 @@
# https://travis-ci.org/jkbrzt/httpie # https://travis-ci.org/jkbrzt/httpie
sudo: false sudo: false
language: python
os: os:
- linux - linux
# - osx
env: env:
global: global:
- NEWEST_PYTHON=3.4 - NEWEST_PYTHON=3.5
language: python
python: python:
- 2.6 - 2.6
- 2.7 - 2.7
- pypy - pypy
- 3.3
- 3.4 - 3.4
- pypy3 - 3.5
# Currently fails because of a Flask issue
# - pypy3
matrix:
include:
# Manually defined OS X builds
# https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)
# Stock OSX Python
- os: osx
language: generic
env:
- TOXENV=py27
# Latest Python 2.x from Homebrew
- 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
# Python Codestyle
- os: linux
python: 3.5
env: CODESTYLE=true
install:
- |
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
if [[ -n "$BREW_INSTALL" ]]; then
brew update
brew install "$BREW_INSTALL"
fi
sudo pip install tox
fi
if [[ $CODESTYLE ]]; then
pip install pycodestyle
fi
script: script:
- make - |
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
if [[ $CODESTYLE ]]; then
# 241 - multiple spaces after ,
# 501 - line too long
pycodestyle --ignore=E241,E501
else
make
fi
else
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
fi
after_success: after_success:
- if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON ]]; then pip install python-coveralls && coveralls; fi - |
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
pip install python-coveralls && coveralls
fi
notifications:
webhooks:
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

View File

@@ -8,6 +8,8 @@ HTTPie authors
Patches and ideas Patches and ideas
----------------- -----------------
`Complete list of contributors on GitHub <https://github.com/jkbrzt/httpie/graphs/contributors>`_
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo) * `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
* `Hank Gay <https://github.com/gthank>`_ * `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_ * `Jake Basile <https://github.com/jakebasile>`_
@@ -34,3 +36,5 @@ Patches and ideas
* `Dennis Brakhane <https://github.com/brakhane>`_ * `Dennis Brakhane <https://github.com/brakhane>`_
* `Matt Layman <https://github.com/mblayman>`_ * `Matt Layman <https://github.com/mblayman>`_
* `Edward Yang <https://github.com/honorabrutroll>`_ * `Edward Yang <https://github.com/honorabrutroll>`_

View File

@@ -6,9 +6,61 @@ This document records all notable changes to `HTTPie <http://httpie.org>`_.
This project adheres to `Semantic Versioning <http://semver.org/>`_. This project adheres to `Semantic Versioning <http://semver.org/>`_.
`0.9.3`_ (2016-01-01) `1.0.0-dev`_ (Unreleased)
------------------------- -------------------------
`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)
---------------------
* Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests
* Added ``--ssl=<PROTOCOL>`` to specify the desired SSL/TLS protocol version
to use for HTTPS requests.
* Added JSON detection with ``--json, -j`` to work around incorrect
``Content-Type``
* Added ``--all`` to show intermediate responses such as redirects (with ``--follow``)
* Added ``--history-print, -P WHAT`` to specify formatting of intermediate responses
* Added ``--max-redirects=N`` (default 30)
* Added ``-A`` as short name for ``--auth-type``
* Added ``-F`` as short name for ``--follow``
* Removed the ``implicit_content_type`` config option
(use ``"default_options": ["--form"]`` instead)
* Redirected ``stdout`` doesn't trigger an error anymore when ``--output FILE``
is set
* Changed the default ``--style`` back to ``solarized`` for better support
of light and dark terminals
* Improved ``--debug`` output
* Fixed ``--session`` when used with ``--download``
* Fixed ``--download`` to trim too long filenames before saving the file
* Fixed the handling of ``Content-Type`` with multiple ``+subtype`` parts
* Removed the XML formatter as the implementation suffered from multiple issues
`0.9.3`_ (2016-01-01)
---------------------
* Changed the default color ``--style`` from ``solarized`` to ``monokai`` * Changed the default color ``--style`` from ``solarized`` to ``monokai``
* Added basic Bash autocomplete support (need to be installed manually) * Added basic Bash autocomplete support (need to be installed manually)
* Added request details to connection error messages * Added request details to connection error messages
@@ -257,4 +309,6 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
.. _0.9.1: https://github.com/jkbrzt/httpie/compare/0.9.0...0.9.1 .. _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.2: https://github.com/jkbrzt/httpie/compare/0.9.1...0.9.2
.. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3 .. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.3...master .. _0.9.4: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.4
.. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.6
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.4...master

View File

@@ -1,12 +1,13 @@
######################
Contributing to HTTPie Contributing to HTTPie
###################### ######################
Bug reports and code and documentation patches are greatly appreciated. You can Bug reports and code and documentation patches are welcome. You can
also help by using the development version of HTTPie and reporting any bugs you help this project also by using the development version of HTTPie
might encounter. and by reporting any bugs you might encounter.
Bug Reports 1. Reporting bugs
=========== =================
**It's important that you provide the full command argument list **It's important that you provide the full command argument list
as well as the output of the failing command.** as well as the output of the failing command.**
@@ -15,12 +16,12 @@ to your bug report, e.g.:
.. code-block:: bash .. code-block:: bash
$ http --debug [arguments that trigger the error] $ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR]
[complete output] [COMPLETE OUTPUT]
Contributing Code and Documentation 2. Contributing Code and Docs
=================================== =============================
Before working on a new feature or a bug, please browse `existing issues`_ 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 to see whether it has been previously discussed. If the change in question
@@ -28,8 +29,11 @@ is a bigger one, it's always good to discuss before your starting working on
it. it.
Development Environment Creating Development Environment
----------------------- --------------------------------
Go to https://github.com/jkbrzt/httpie and fork the project repository.
.. code-block:: bash .. code-block:: bash
@@ -52,44 +56,61 @@ 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).
Tests Testing
----- -------
Before opening a pull requests, please make sure the `test suite`_ passes Before opening a pull requests, please make sure the `test suite`_ passes
in all of the `supported Python environments`_. You should also **add tests in all of the `supported Python environments`_. You should also add tests
for any new features and bug fixes**. for any new features and bug fixes.
HTTPie uses `pytest`_ and `Tox`_. HTTPie uses `pytest`_ and `Tox`_ for testing.
Running all tests:
******************
.. code-block:: bash .. code-block:: bash
### Running all tests: # Run all tests on the current Python interpreter
# Current Python
make test make test
# Current Python with coverage # Run all tests on the current Python with coverage
make test-cover make test-cover
# All the supported and available Pythons via Tox # Run all tests in all of the supported and available Pythons via Tox
make test-tox make test-tox
### Running specific tests: # Run all tests for code as well as packaging, etc.
make test-all
# Current Python
pytest tests/test_uploads.py
# All Pythons Running specific tests:
***********************
.. code-block:: bash
# Run specific tests on the current Python
py.test tests/test_uploads.py
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
# Run specific tests on the on all Pythons via Tox
tox -- tests/test_uploads.py --verbose tox -- tests/test_uploads.py --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
Don't forget to add yourself to `AUTHORS.rst`_. -----
See `Makefile`_ for additional development utilities.
Don't forget to add yourself to `AUTHORS`_!
.. _Tox: http://tox.testrun.org .. _Tox: http://tox.testrun.org
.. _supported Python environments: https://github.com/jkbrzt/httpie/blob/master/tox.ini .. _supported Python environments: https://github.com/jkbrzt/httpie/blob/master/tox.ini
.. _existing issues: https://github.com/jkbrzt/httpie/issues?state=open .. _existing issues: https://github.com/jkbrzt/httpie/issues?state=open
.. _AUTHORS.rst: https://github.com/jkbrzt/httpie/blob/master/AUTHORS.rst .. _AUTHORS: https://github.com/jkbrzt/httpie/blob/master/AUTHORS.rst
.. _Makefile: https://github.com/jkbrzt/httpie/blob/master/Makefile
.. _pytest: http://pytest.org/ .. _pytest: http://pytest.org/
.. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/ .. _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/jkbrzt/httpie/tree/master/tests

View File

@@ -1,4 +1,4 @@
Copyright © 2012-2015 Jakub Roztocil <jakub@roztocil.co> Copyright © 2012-2016 Jakub Roztocil <jakub@roztocil.co>
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

View File

@@ -1,39 +1,42 @@
#
# See ./CONTRIBUTING.rst
#
VERSION=$(shell grep __version__ httpie/__init__.py) VERSION=$(shell grep __version__ httpie/__init__.py)
REQUIREMENTS="requirements-dev.txt" REQUIREMENTS="requirements-dev.txt"
TAG="\n\n\033[0;32m\#\#\# " TAG="\n\n\033[0;32m\#\#\# "
END=" \#\#\# \033[0m\n" END=" \#\#\# \033[0m\n"
all: test all: test
uninstall-httpie:
@echo $(TAG)Removing existing installation of HTTPie$(END)
- pip uninstall --yes httpie >/dev/null
! which http
@echo
uninstall-all: uninstall-httpie
- pip uninstall --yes -r $(REQUIREMENTS)
init: uninstall-httpie init: uninstall-httpie
@echo $(TAG)Installing dev requirements$(END) @echo $(TAG)Installing dev requirements$(END)
pip install --upgrade -r $(REQUIREMENTS) pip install --upgrade -r $(REQUIREMENTS)
@echo $(TAG)Installing HTTPie$(END) @echo $(TAG)Installing HTTPie$(END)
pip install --upgrade --editable . pip install --upgrade --editable .
@echo @echo
test: init test: init
@echo $(TAG)Running tests in on current Python with coverage $(END) @echo $(TAG)Running tests on the current Python interpreter with coverage $(END)
py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests
@echo @echo
test-tox: init test-tox: init
@echo $(TAG)Running tests on all Pythons via Tox$(END) @echo $(TAG)Running tests on all Pythons via Tox$(END)
tox tox
@echo @echo
test-dist: test-sdist test-bdist-wheel test-dist: test-sdist test-bdist-wheel
@echo @echo
test-sdist: clean uninstall-httpie test-sdist: clean uninstall-httpie
@echo $(TAG)Testing sdist build an installation$(END) @echo $(TAG)Testing sdist build an installation$(END)
python setup.py sdist python setup.py sdist
@@ -41,6 +44,7 @@ test-sdist: clean uninstall-httpie
which http which http
@echo @echo
test-bdist-wheel: clean uninstall-httpie test-bdist-wheel: clean uninstall-httpie
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
python setup.py bdist_wheel python setup.py bdist_wheel
@@ -48,20 +52,49 @@ test-bdist-wheel: clean uninstall-httpie
which http which http
@echo @echo
# This tests everything, even this Makefile. # This tests everything, even this Makefile.
test-all: uninstall-all clean init test test-tox test-dist test-all: uninstall-all clean init test test-tox test-dist
publish: test-all
publish: test-all publish-no-test
publish-no-test:
@echo $(TAG)Testing wheel build an installation$(END) @echo $(TAG)Testing wheel build an installation$(END)
@echo "$(VERSION)" @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 register
python setup.py sdist upload python setup.py sdist upload
python setup.py bdist_wheel upload python setup.py bdist_wheel upload
@echo @echo
clean: clean:
@echo $(TAG)Cleaning up$(END) @echo $(TAG)Cleaning up$(END)
rm -rf .tox *.egg dist build .coverage rm -rf .tox *.egg dist build .coverage
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
@echo @echo
uninstall-httpie:
@echo $(TAG)Uninstalling httpie$(END)
- pip uninstall --yes httpie &2>/dev/null
@echo "Verifying…"
cd .. && ! python -m httpie --version &2>/dev/null
@echo "Done"
@echo
uninstall-all: uninstall-httpie
@echo $(TAG)Uninstalling httpie requirements$(END)
- pip uninstall --yes pygments requests
@echo $(TAG)Uninstalling development requirements$(END)
- pip uninstall --yes -r $(REQUIREMENTS)
homebrew-formula-vars:
extras/get-homebrew-formula-vars.py

View File

@@ -23,7 +23,7 @@ HTTPie is written in Python, and under the hood it uses the excellent
----- -----
|pypi| |unix_build| |windows_build| |coverage| |pypi| |unix_build| |windows_build| |coverage| |gitter|
----- -----
@@ -35,7 +35,7 @@ HTTPie is written in Python, and under the hood it uses the excellent
============= =============
Main Features Main features
============= =============
* Expressive and intuitive syntax * Expressive and intuitive syntax
@@ -59,13 +59,21 @@ Installation
============ ============
On **Mac OS X**, HTTPie can be installed via `Homebrew <http://brew.sh/>`_: On **Mac OS X**, HTTPie can be installed via `Homebrew <http://brew.sh/>`_
(recommended):
.. code-block:: bash .. code-block:: bash
$ brew install httpie $ brew install httpie
A MacPorts *port* is also available:
.. code-block:: bash
$ port install httpie
Most **Linux** distributions provide a package that can be installed using the Most **Linux** distributions provide a package that can be installed using the
system package manager, e.g.: system package manager, e.g.:
@@ -106,9 +114,20 @@ The **latest development version** can be installed directly from GitHub:
$ brew install httpie --HEAD $ brew install httpie --HEAD
# Universal # Universal
$ pip install --upgrade https://github.com/jkbrzt/httpie/tarball/master $ pip install --upgrade https://github.com/jkbrzt/httpie/archive/master.tar.gz
--------------
Python version
--------------
Although Python 2.6 and 2.7 are supported as well, it is recommended to install
HTTPie against the latest Python 3.x whenever possible. That will ensure that
some of the newer HTTP features, such as `SNI (Server Name Indication)`_,
work out of the box.
Python 3 is the default for Homebrew installations starting with version 0.9.4.
To see which version HTTPie uses, run ``http --debug``.
===== =====
Usage Usage
@@ -164,7 +183,7 @@ with `authentication`_:
.. code-block:: bash .. code-block:: bash
$ http -a USERNAME POST https://api.github.com/repos/jkbrzt/httpie/issues/83/comments body='HTTPie is awesome!' $ http -a USERNAME POST https://api.github.com/repos/jkbrzt/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
Upload a file using `redirected input`_: Upload a file using `redirected input`_:
@@ -212,7 +231,7 @@ advanced usage, and also features additional examples.*
=========== ===========
HTTP Method HTTP method
=========== ===========
The name of the HTTP method comes right before the URL argument: The name of the HTTP method comes right before the URL argument:
@@ -279,22 +298,31 @@ If the port is omitted, then port 80 is assumed.
If you find yourself manually constructing URLs with **querystring parameters** If you find yourself manually constructing URLs with **querystring parameters**
on the terminal, you may appreciate the ``param==value`` syntax for appending on the terminal, you may appreciate the ``param==value`` syntax for appending
URL parameters so that you don't have to worry about escaping the ``&`` URL parameters. With that, you don't have to worry about escaping the ``&``
separators. To search for ``HTTPie`` on Google Images you could use this separators for you shell. Also, special characters in parameter values,
command: will also automatically escaped (HTTPie otherwise expects the URL to be
already escaped). To search for ``HTTPie logo`` on Google Images you could use
this command:
.. code-block:: bash .. code-block:: bash
$ http GET www.google.com search==HTTPie tbm==isch $ http www.google.com search=='HTTPie logo' tbm==isch
.. code-block:: http .. code-block:: http
GET /?search=HTTPie&tbm=isch HTTP/1.1 GET /?search=HTTPie+logo&tbm=isch HTTP/1.1
You can use the ``--default-scheme <URL_SCHEME>`` option to create
shortcuts for other protocols than HTTP:
.. code-block:: bash
$ alias https='http --default-scheme=https'
============= =============
Request Items Request items
============= =============
There are a few different *request item* types that provide a There are a few different *request item* types that provide a
@@ -333,11 +361,29 @@ their type is distinguished only by the separator used:
| | in a ``multipart/form-data`` request. | | | in a ``multipart/form-data`` request. |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
You can use ``\`` to escape characters that shouldn't be used as separators You can use ``\`` to escape characters that shouldn't be used as separators
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value (or parts thereof). For instance, ``foo\==bar`` will become a data key/value
pair (``foo=`` and ``bar``) instead of a URL parameter. pair (``foo=`` and ``bar``) instead of a URL parameter.
You can also quote values, e.g. ``foo="bar baz"``. Often it is necessary to quote the values, e.g. ``foo='bar baz'``.
If any of the field names or headers starts with a minus
(e.g., ``-fieldname``), you need to place all such items after the special
token ``--`` to prevent confusion with ``--arguments``:
.. code-block:: bash
$ http httpbin.org/post -- -name-starting-with-dash=foo --Weird-Header:bar
.. code-block:: http
POST /post HTTP/1.1
--Weird-Header: bar
{
"-name-starting-with-dash": "value"
}
Note that data fields aren't the only way to specify request data: Note that data fields aren't the only way to specify request data:
`Redirected input`_ allows for passing arbitrary data to be sent with the `Redirected input`_ allows for passing arbitrary data to be sent with the
@@ -357,13 +403,15 @@ both of which can be overwritten:
================ ======================================= ================ =======================================
``Content-Type`` ``application/json`` ``Content-Type`` ``application/json``
``Accept`` ``application/json`` ``Accept`` ``application/json, */*``
================ ======================================= ================ =======================================
You can use ``--json, -j`` to explicitly set ``Accept`` You can use ``--json, -j`` to explicitly set ``Accept``
to ``application/json`` regardless of whether you are sending data to ``application/json`` regardless of whether you are sending data
(it's a shortcut for setting the header via the usual header notation (it's a shortcut for setting the header via the usual header notation
``http url Accept:application/json``). ``http url Accept:application/json, */*``). Additionally,
HTTPie will try to detect JSON responses even when the
``Content-Type`` is incorrectly ``text/plain`` or unknown.
Simple example: Simple example:
@@ -374,7 +422,7 @@ Simple example:
.. code-block:: http .. code-block:: http
PUT / HTTP/1.1 PUT / HTTP/1.1
Accept: application/json Accept: application/json, */*
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
Content-Type: application/json Content-Type: application/json
Host: example.org Host: example.org
@@ -401,7 +449,7 @@ fields using ``=@`` and ``:=@``:
.. code-block:: http .. code-block:: http
PUT /person/1 HTTP/1.1 PUT /person/1 HTTP/1.1
Accept: application/json Accept: application/json, */*
Content-Type: application/json Content-Type: application/json
Host: api.example.com Host: api.example.com
@@ -441,7 +489,7 @@ via the `config`_ file.
------------- -------------
Regular Forms Regular forms
------------- -------------
.. code-block:: bash .. code-block:: bash
@@ -459,7 +507,7 @@ Regular Forms
----------------- -----------------
File Upload Forms File upload forms
----------------- -----------------
If one or more file fields is present, the serialization and content type is If one or more file fields is present, the serialization and content type is
@@ -485,7 +533,7 @@ Note that ``@`` is used to simulate a file upload form field, whereas
============ ============
HTTP Headers HTTP headers
============ ============
To set custom headers you can use the ``Header:Value`` notation: To set custom headers you can use the ``Header:Value`` notation:
@@ -519,7 +567,23 @@ There are a couple of default headers that HTTPie sets:
Host: <taken-from-URL> Host: <taken-from-URL>
Any of the default headers can be overwritten. Any of the default headers can be overwritten and some of them unset.
To unset a header that has already been specified (such a one of the default
headers), use ``Header:``:
.. code-block:: bash
$ http httpbin.org/headers Accept: User-Agent:
To send a header with an empty value, use ``Header;``:
.. code-block:: bash
$ http httpbin.org/headers 'Header;'
============== ==============
@@ -539,7 +603,7 @@ The currently supported authentication schemes are Basic and Digest
supported as well (but credentials passed via ``-a`` supported as well (but credentials passed via ``-a``
have higher priority). have higher priority).
``--auth-type`` Specify the auth mechanism. Possible values are ``--auth-type, -A`` Specify the auth mechanism. Possible values are
``basic`` and ``digest``. The default value is ``basic`` and ``digest``. The default value is
``basic`` so it can often be omitted. ``basic`` so it can often be omitted.
=================== ====================================================== =================== ======================================================
@@ -559,7 +623,7 @@ Digest auth:
.. code-block:: bash .. code-block:: bash
$ http --auth-type=digest -a username:password example.org $ http -A digest -a username:password example.org
With password prompt: With password prompt:
@@ -584,10 +648,11 @@ Authorization information from your ``~/.netrc`` file is honored as well:
------------ ------------
Auth Plugins Auth plugins
------------ ------------
* `httpie-oauth <https://github.com/jkbrzt/httpie-oauth>`_: OAuth * `httpie-oauth <https://github.com/jkbrzt/httpie-oauth>`_: OAuth
* `httpie-hmac-auth <https://github.com/guardian/httpie-hmac-auth>`_: HMAC
* `httpie-ntlm <https://github.com/jkbrzt/httpie-ntlm>`_: NTLM (NT LAN Manager) * `httpie-ntlm <https://github.com/jkbrzt/httpie-ntlm>`_: NTLM (NT LAN Manager)
* `httpie-negotiate <https://github.com/ndzou/httpie-negotiate>`_: SPNEGO (GSS Negotiate) * `httpie-negotiate <https://github.com/ndzou/httpie-negotiate>`_: SPNEGO (GSS Negotiate)
* `requests-hawk <https://github.com/mozilla-services/requests-hawk>`_: Hawk * `requests-hawk <https://github.com/mozilla-services/requests-hawk>`_: Hawk
@@ -596,6 +661,26 @@ Auth Plugins
* `httpie-jwt-auth <https://github.com/teracyhq/httpie-jwt-auth>`_: JWTAuth (JSON Web Tokens) * `httpie-jwt-auth <https://github.com/teracyhq/httpie-jwt-auth>`_: JWTAuth (JSON Web Tokens)
==============
HTTP redirects
==============
By default, HTTP redirects are not followed and only the first
response is shown. To instruct HTTPie to follow the ``Location`` header of
``30x`` responses and show the final response instead, use the ``--follow, -F`` option.
If you additionally wish to see the intermediary requests/responses,
then use the ``--all`` option as well.
To change the default limit of maximum 30 redirects, use the
``--max-redirects=<limit>`` option.
.. code-block:: bash
$ http --follow --all --max-redirects=5 httpbin.org/redirect/3
======= =======
Proxies Proxies
======= =======
@@ -628,6 +713,24 @@ In your ``~/.bash_profile``:
export NO_PROXY=localhost,example.com export NO_PROXY=localhost,example.com
-----
SOCKS
-----
To enable SOCKS proxy support please install ``requests[socks]`` using ``pip``:
.. code-block:: bash
$ pip install -U requests[socks]
Usage is the same as for other types of `proxies`_:
.. code-block:: bash
$ http --proxy=http:socks5://user:pass@host:port --proxy=https:socks5://user:pass@host:port example.org
===== =====
HTTPS HTTPS
===== =====
@@ -679,12 +782,28 @@ path of the key file with ``--cert-key``:
$ http --cert=client.crt --cert-key=client.key https://example.org $ http --cert=client.crt --cert-key=client.key https://example.org
-----------
SSL version
-----------
Use the ``--ssl=<PROTOCOL>`` to specify the desired protocol version to use.
This will default to SSL v2.3 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support. The available protocols
are ``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ``tls1.2``. (The actually
available set of protocols may vary depending on your OpenSSL installation.)
.. code-block:: bash
# Specify the vulnerable SSL v3 protocol to talk to an outdated server:
$ http --ssl=ssl3 https://vulnerable.example.org
---------------------------- ----------------------------
SNI (Server Name Indication) SNI (Server Name Indication)
---------------------------- ----------------------------
If you use HTTPie with Python < 2.7.9 If you use HTTPie with `Python version`_ lower than 2.7.9
(can be verified with ``python --version``) and need to talk to servers that (can be verified with ``http --debug``) and need to talk to servers that
use **SNI (Server Name Indication)** you need to install some additional use **SNI (Server Name Indication)** you need to install some additional
dependencies: dependencies:
@@ -701,11 +820,11 @@ You can use the following command to test SNI support:
============== ==============
Output Options Output options
============== ==============
By default, HTTPie outputs the whole response message (headers as well as the By default, HTTPie only outputs the final response and the whole response
body). message is printed (headers as well as the body).
You can control what should be printed via several options: You can control what should be printed via several options:
@@ -713,6 +832,7 @@ You can control what should be printed via several options:
``--headers, -h`` Only the response headers are printed. ``--headers, -h`` Only the response headers are printed.
``--body, -b`` Only the response body is printed. ``--body, -b`` Only the response body is printed.
``--verbose, -v`` Print the whole HTTP exchange (request and response). ``--verbose, -v`` Print the whole HTTP exchange (request and response).
This option also enables ``--all`` (see bellow).
``--print, -p`` Selects parts of the HTTP exchange. ``--print, -p`` Selects parts of the HTTP exchange.
================= ===================================================== ================= =====================================================
@@ -723,7 +843,7 @@ documentation examples:
$ http --verbose PUT httpbin.org/put hello=world $ http --verbose PUT httpbin.org/put hello=world
PUT /put HTTP/1.1 PUT /put HTTP/1.1
Accept: application/json Accept: application/json, */*
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
Content-Type: application/json Content-Type: application/json
Host: httpbin.org Host: httpbin.org
@@ -753,10 +873,10 @@ the HTTP exchange:
========== ================== ========== ==================
Character Stands for Character Stands for
========== ================== ========== ==================
``H`` Request headers. ``H`` request headers
``B`` Request body. ``B`` request body
``h`` Response headers. ``h`` response headers
``b`` Response body. ``b`` response body
========== ================== ========== ==================
Print request and response headers: Print request and response headers:
@@ -766,8 +886,36 @@ Print request and response headers:
$ http --print=Hh PUT httpbin.org/put hello=world $ http --print=Hh PUT httpbin.org/put hello=world
---------------------------------------
Viewing intermediary requests/responses
---------------------------------------
To see *all* the HTTP communication, i.e. the final request/response as
well as any possible intermediary requests/responses, use the ``--all``
option. The intermediary HTTP communication include followed redirects
(with ``--follow``), the first unauthorized request when HTTP digest
authentication is used (``--auth=digest``), etc.
.. code-block:: bash
# Include all responses that lead to the final one:
$ http --all --follow httpbin.org/redirect/3
The intermediary requests/response are by default formatted according to
``--print, -p`` (and its shortcuts described above). If you'd like to change
that, use the ``--history-print, -P`` option. It takes the same
arguments as ``--print, -p`` but applies to the intermediary requests only.
.. code-block:: bash
# Print the intermediary requests/responses differently than the final one:
$ http -A digest -a foo:bar --all -p Hh -P H httpbin.org/digest-auth/auth/foo/bar
------------------------- -------------------------
Conditional Body Download Conditional body download
------------------------- -------------------------
As an optimization, the response body is downloaded from the server As an optimization, the response body is downloaded from the server
@@ -865,9 +1013,9 @@ To prevent HTTPie from reading ``stdin`` data you can use the
``--ignore-stdin`` option. ``--ignore-stdin`` option.
------------------------- ----------------------------
Body Data From a Filename Request data from a filename
------------------------- ----------------------------
**An alternative to redirected** ``stdin`` is specifying a filename (as **An alternative to redirected** ``stdin`` is specifying a filename (as
``@/path/to/file``) whose content is used as if it came from ``stdin``. ``@/path/to/file``) whose content is used as if it came from ``stdin``.
@@ -883,7 +1031,7 @@ verbatim contents of that XML file with ``Content-Type: application/xml``:
=============== ===============
Terminal Output Terminal output
=============== ===============
HTTPie does several things by default in order to make its terminal output HTTPie does several things by default in order to make its terminal output
@@ -891,7 +1039,7 @@ easy to read.
--------------------- ---------------------
Colors and Formatting Colors and formatting
--------------------- ---------------------
Syntax highlighting is applied to HTTP headers and bodies (where it makes Syntax highlighting is applied to HTTP headers and bodies (where it makes
@@ -904,7 +1052,6 @@ Also, the following formatting is applied:
* HTTP headers are sorted by name. * HTTP headers are sorted by name.
* JSON data is indented, sorted by keys, and unicode escapes are converted * JSON data is indented, sorted by keys, and unicode escapes are converted
to the characters they represent. to the characters they represent.
* XML data is indented for better readability.
One of these options can be used to control output processing: One of these options can be used to control output processing:
@@ -947,7 +1094,7 @@ You will nearly instantly see something like this:
================= =================
Redirected Output Redirected output
================= =================
HTTPie uses **different defaults** for redirected output than for HTTPie uses **different defaults** for redirected output than for
@@ -998,7 +1145,7 @@ by adding the following to your ``~/.bash_profile``:
============= =============
Download Mode Download mode
============= =============
HTTPie features a download mode in which it acts similarly to ``wget``. HTTPie features a download mode in which it acts similarly to ``wget``.
@@ -1009,20 +1156,17 @@ is being saved to a file.
.. code-block:: bash .. code-block:: bash
$ http --download https://github.com/jkbrzt/httpie/tarball/master $ http --download https://github.com/jkbrzt/httpie/archive/master.tar.gz
.. code-block:: http .. code-block:: http
HTTP/1.1 200 OK HTTP/1.1 200 OK
Connection: keep-alive Content-Disposition: attachment; filename=httpie-master.tar.gz
Content-Disposition: attachment; filename=jkbrzt-httpie-0.4.1-33-gfc4f70a.tar.gz Content-Length: 257336
Content-Length: 505530
Content-Type: application/x-gzip Content-Type: application/x-gzip
Server: GitHub.com
Vary: Accept-Encoding
Downloading 494.89 kB to "jkbrzt-httpie-0.4.1-33-gfc4f70a.tar.gz" Downloading 251.30 kB to "httpie-master.tar.gz"
/ 21.01% 104.00 kB 47.55 kB/s 0:00:08 ETA Done. 251.30 kB in 2.73862s (91.76 kB/s)
If not provided via ``--output, -o``, the output filename will be determined If not provided via ``--output, -o``, the output filename will be determined
@@ -1035,7 +1179,7 @@ headers and progress are still shown in the terminal:
.. code-block:: bash .. code-block:: bash
$ http -d https://github.com/jkbrzt/httpie/tarball/master | tar zxf - $ http -d https://github.com/jkbrzt/httpie/archive/master.tar.gz | tar zxf -
If ``--output, -o`` is specified, you can resume a partial download using the If ``--output, -o`` is specified, you can resume a partial download using the
@@ -1058,7 +1202,7 @@ Other notes:
================== ==================
Streamed Responses Streamed responses
================== ==================
Responses are downloaded and printed in chunks, which allows for streaming Responses are downloaded and printed in chunks, which allows for streaming
@@ -1105,7 +1249,7 @@ ones starting with ``Content-`` or ``If-``), authorization, and cookies
to the same host. to the same host.
-------------- --------------
Named Sessions Named sessions
-------------- --------------
Create a new session named ``user1`` for ``example.org``: Create a new session named ``user1`` for ``example.org``:
@@ -1136,7 +1280,7 @@ Named sessions' data is stored in JSON files in the directory
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows). (``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
------------------ ------------------
Anonymous Sessions Anonymous sessions
------------------ ------------------
Instead of a name, you can also directly specify a path to a session file. This Instead of a name, you can also directly specify a path to a session file. This
@@ -1166,36 +1310,32 @@ Config
HTTPie uses a simple configuration file that contains a JSON object with the HTTPie uses a simple configuration file that contains a JSON object with the
following keys: following keys:
========================= =================================================
``__meta__`` HTTPie automatically stores some metadata here.
Do not change.
``implicit_content_type`` A ``String`` specifying the implicit content type ------------
for request data. The default value for this ``__meta__``
option is ``json`` and can be changed to ------------
``form``.
``default_options`` An ``Array`` (by default empty) of options HTTPie automatically stores some of its metadata here. Do not change.
that should be applied to every request.
For instance, you can use this option to change
the default style and output options:
``"default_options": ["--style=fruity", "--body"]``
Another useful default option is -------------------
``"--session=default"`` to make HTTPie always ``default_options``
use `sessions`_. -------------------
Default options from config file can be unset An ``Array`` (by default empty) of default options that should be applied to
for a particular invocation via every invocation of HTTPie.
``--no-OPTION`` arguments passed on the
command line (e.g., ``--no-style``
or ``--no-session``).
========================= =================================================
The default location of the configuration file is ``~/.httpie/config.json`` For instance, you can use this option to change the default style and output
(or ``%APPDATA%\httpie\config.json`` on Windows). options: ``"default_options": ["--style=fruity", "--body"]`` Another useful
default option could be ``"--session=default"`` to make HTTPie always
use `sessions`_ (one named ``default`` will automatically be used).
Or you could change the implicit request content type from JSON to form by
adding ``--form`` to the list.
Default options from config file can be unset for a particular invocation via
``--no-OPTION`` arguments passed on the command line (e.g., ``--no-style``
or ``--no-session``). The default location of the configuration file is
``~/.httpie/config.json`` (or ``%APPDATA%\httpie\config.json`` on Windows).
The config directory location can be changed by setting the The config directory location can be changed by setting the
``HTTPIE_CONFIG_DIR`` environment variable. ``HTTPIE_CONFIG_DIR`` environment variable.
@@ -1227,13 +1367,14 @@ Also, the ``--timeout`` option allows to overwrite the default 30s timeout:
3) echo 'Unexpected HTTP 3xx Redirection!' ;; 3) echo 'Unexpected HTTP 3xx Redirection!' ;;
4) echo 'HTTP 4xx Client Error!' ;; 4) echo 'HTTP 4xx Client Error!' ;;
5) echo 'HTTP 5xx Server Error!' ;; 5) echo 'HTTP 5xx Server Error!' ;;
6) echo 'Exceeded --max-redirects=<n> redirects!' ;;
*) echo 'Other Error!' ;; *) echo 'Other Error!' ;;
esac esac
fi fi
================ ================
Interface Design Interface design
================ ================
The syntax of the command arguments closely corresponds to the actual HTTP The syntax of the command arguments closely corresponds to the actual HTTP
@@ -1279,6 +1420,21 @@ HTTPie reaches its final version ``1.0``. All changes are recorded in the
=======
Support
=======
Please use the following support channels:
* `GitHub issues <https://github.com/jkbr/httpie/issues>`_
for bug reports and feature requests.
* `Our Gitter chat room <https://gitter.im/jkbrzt/httpie>`_
to ask questions, discuss features, and for general discussion.
* `StackOverflow <https://stackoverflow.com>`_
to ask questions (please make sure to use the
`httpie <http://stackoverflow.com/questions/tagged/httpie>`_ tag).
* You can also tweet directly to `@jkbrzt`_.
======= =======
Authors Authors
======= =======
@@ -1292,28 +1448,39 @@ have contributed.
Logo Logo
==== ====
Please see `claudiatd/httpie-artwork`_ See `claudiatd/httpie-artwork`_
========== ==========
Contribute Contribute
========== ==========
Please see `CONTRIBUTING <https://github.com/jkbrzt/httpie/blob/master/CONTRIBUTING.rst>`_. See `CONTRIBUTING <https://github.com/jkbrzt/httpie/blob/master/CONTRIBUTING.rst>`_.
========== ==========
Change Log Change log
========== ==========
Please see `CHANGELOG <https://github.com/jkbrzt/httpie/blob/master/CHANGELOG.rst>`_. See `CHANGELOG <https://github.com/jkbrzt/httpie/blob/master/CHANGELOG.rst>`_.
======= =======
Licence Licence
======= =======
Please see `LICENSE <https://github.com/jkbrzt/httpie/blob/master/LICENSE>`_. See `LICENSE <https://github.com/jkbrzt/httpie/blob/master/LICENSE>`_.
================
Related projects
================
* `jq <https://stedolan.github.io/jq/>`_ — a command-line JSON processor that
works great in conjunction with HTTPie
* `http-prompt <https://github.com/eliangcs/http-prompt>`_ — an interactive
shell for HTTPie featuring autocomplete and command syntax highlighting
@@ -1327,11 +1494,11 @@ Please see `LICENSE <https://github.com/jkbrzt/httpie/blob/master/LICENSE>`_.
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork .. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20version .. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version
:target: https://pypi.python.org/pypi/httpie :target: https://pypi.python.org/pypi/httpie
:alt: Latest version released on PyPi :alt: Latest version released on PyPi
.. |coverage| image:: https://img.shields.io/coveralls/jkbrzt/httpie/master.svg?style=flat-square .. |coverage| image:: https://img.shields.io/coveralls/jkbrzt/httpie/master.svg?style=flat-square&label=coverage
:target: https://coveralls.io/r/jkbrzt/httpie?branch=master :target: https://coveralls.io/r/jkbrzt/httpie?branch=master
:alt: Test coverage :alt: Test coverage
@@ -1342,3 +1509,8 @@ Please see `LICENSE <https://github.com/jkbrzt/httpie/blob/master/LICENSE>`_.
.. |windows_build| image:: https://img.shields.io/appveyor/ci/jkbrzt/httpie.svg?style=flat-square&label=windows%20build .. |windows_build| image:: https://img.shields.io/appveyor/ci/jkbrzt/httpie.svg?style=flat-square&label=windows%20build
:target: https://ci.appveyor.com/project/jkbrzt/httpie :target: https://ci.appveyor.com/project/jkbrzt/httpie
:alt: Build status of the master branch on Windows :alt: Build status of the master branch on Windows
.. |gitter| image:: https://badges.gitter.im/jkbrzt/httpie.svg
:target: https://gitter.im/jkbrzt/httpie
:alt: Chat on Gitter

View File

@@ -1,17 +1,22 @@
# https://ci.appveyor.com/project/jkbrzt/httpie # https://ci.appveyor.com/project/jkbrzt/httpie
build: false build: false
environment: environment:
matrix: matrix:
- PYTHON: "C:/Python27" - PYTHON: "C:/Python27"
- PYTHON: "C:/Python34" # Python 3.4 has outdated pip
# - PYTHON: "C:/Python34"
- PYTHON: "C:/Python35"
init: init:
- "ECHO %PYTHON%" - "ECHO %PYTHON%"
- ps: "ls C:/Python*" - ps: "ls C:/Python*"
install: install:
# - ps: (new-object net.webclient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', 'C:/get-pip.py') # FIXME: updating pip fails with PermissionError
# - "%PYTHON%/python.exe C:/get-pip.py" # - "%PYTHON%/Scripts/pip.exe install -U pip setuptools"
- "%PYTHON%/Scripts/pip.exe install -U pip setuptools"
- "%PYTHON%/Scripts/pip.exe install -e ." - "%PYTHON%/Scripts/pip.exe install -e ."
test_script: test_script:
- "%PYTHON%/Scripts/pip.exe --version" - "%PYTHON%/Scripts/pip.exe --version"
- "%PYTHON%/Scripts/http.exe --debug" - "%PYTHON%/Scripts/http.exe --debug"

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python
"""
Generate URLs and file hashes to be included in the Homebrew formula
after a new release of HTTPie is published on PyPi.
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
"""
import hashlib
import requests
PACKAGES = [
'httpie',
'requests',
'pygments',
]
def get_info(package_name):
api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name)
resp = requests.get(api_url).json()
hasher = hashlib.sha256()
for release in resp['urls']:
download_url = release['url']
if download_url.endswith('.tar.gz'):
hasher.update(requests.get(download_url).content)
return {
'name': package_name,
'url': download_url,
'sha256': hasher.hexdigest(),
}
else:
raise RuntimeError(
'{}: download not found: {}'.format(package_name, resp))
packages = {
package_name: get_info(package_name) for package_name in PACKAGES
}
httpie_info = packages.pop('httpie')
print("""
url "{url}"
sha256 "{sha256}"
""".format(**httpie_info))
for package_info in packages.values():
print("""
resource "{name}" do
url "{url}"
sha256 "{sha256}"
end""".format(**package_info))

View File

@@ -0,0 +1,60 @@
function __fish_httpie_auth_types
echo "basic"\t"Basic HTTP auth"
echo "digest"\t"Digest HTTP auth"
end
function __fish_httpie_styles
echo "autumn"
echo "borland"
echo "bw"
echo "colorful"
echo "default"
echo "emacs"
echo "friendly"
echo "fruity"
echo "igor"
echo "manni"
echo "monokai"
echo "murphy"
echo "native"
echo "paraiso-dark"
echo "paraiso-light"
echo "pastie"
echo "perldoc"
echo "rrt"
echo "solarized"
echo "tango"
echo "trac"
echo "vim"
echo "vs"
echo "xcode"
end
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode"
complete -c http -s f -l form -d 'Data items from the command line are serialized as form fields'
complete -c http -s j -l json -d '(default) Data items from the command line are serialized as a JSON object'
complete -x -c http -l pretty -d 'Controls output processing' -a "all colors format none" -A
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a "autumn borland bw colorful default emacs friendly fruity igor manni monokai murphy native paraiso-dark paraiso-light pastie perldoc rrt solarized tango trac vim vs xcode"
complete -x -c http -s p -l print -d 'String specifying what the output should contain'
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
complete -c http -s h -l headers -d 'Print only the response headers'
complete -c http -s b -l body -d 'Print only the response body'
complete -c http -s S -l stream -d 'Always stream the output by line'
complete -c http -s o -l output -d 'Save output to FILE'
complete -c http -s d -l download -d 'Do not print the response body to stdout'
complete -c http -s c -l continue -d 'Resume an interrupted download'
complete -x -c http -l session -d 'Create, or reuse and update a session'
complete -x -c http -s a -l auth -d 'If only the username is provided (-a username), HTTPie will prompt for the password'
complete -x -c http -l auth-type -d 'The authentication mechanism to be used' -a '(__fish_httpie_auth_types)' -A
complete -x -c http -l proxy -d 'String mapping protocol to the URL of the proxy'
complete -c http -l follow -d 'Allow full redirects'
complete -x -c http -l verify -d 'SSL cert verification'
complete -c http -l cert -d 'SSL cert'
complete -c http -l cert-key -d 'Private SSL cert key'
complete -x -c http -l timeout -d 'Connection timeout in seconds'
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
complete -c http -l ignore-stdin -d 'Do not attempt to read stdin'
complete -c http -l help -d 'Show help'
complete -c http -l version -d 'Show version'
complete -c http -l traceback -d 'Prints exception traceback should one occur'
complete -c http -l debug -d 'Show debugging information'

View File

@@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
""" """
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.9.3' __version__ = '0.9.6'
__licence__ = 'BSD' __licence__ = 'BSD'
@@ -12,8 +12,16 @@ class ExitStatus:
OK = 0 OK = 0
ERROR = 1 ERROR = 1
ERROR_TIMEOUT = 2 ERROR_TIMEOUT = 2
ERROR_TOO_MANY_REDIRECTS = 6
# Used only when requested with --check-status: # Used only when requested with --check-status:
ERROR_HTTP_3XX = 3 ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4 ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5 ERROR_HTTP_5XX = 5
EXIT_STATUS_LABELS = dict(
(value, key)
for key, value in ExitStatus.__dict__.items()
if key.isupper()
)

View File

@@ -13,13 +13,14 @@ from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.plugins import plugin_manager from httpie.plugins import plugin_manager
from httpie.sessions import DEFAULT_SESSIONS_DIR from httpie.sessions import DEFAULT_SESSIONS_DIR
from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE
from httpie.input import (Parser, AuthCredentialsArgType, KeyValueArgType, from httpie.input import (HTTPieArgumentParser,
AuthCredentialsArgType, KeyValueArgType,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS, SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS, OUT_RESP_BODY, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
readable_file_arg) readable_file_arg, SSL_VERSION_ARG_MAPPING)
class HTTPieHelpFormatter(RawDescriptionHelpFormatter): class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
@@ -40,7 +41,7 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
text = dedent(text).strip() + '\n\n' text = dedent(text).strip() + '\n\n'
return text.splitlines() return text.splitlines()
parser = Parser( parser = HTTPieArgumentParser(
formatter_class=HTTPieHelpFormatter, formatter_class=HTTPieHelpFormatter,
description='%s <http://httpie.org>' % __doc__.strip(), description='%s <http://httpie.org>' % __doc__.strip(),
epilog=dedent(""" epilog=dedent("""
@@ -51,7 +52,7 @@ parser = Parser(
https://github.com/jkbrzt/httpie/issues https://github.com/jkbrzt/httpie/issues
""") """),
) )
@@ -88,6 +89,7 @@ positional.add_argument(
metavar='URL', metavar='URL',
help=""" help="""
The scheme defaults to 'http://' if the URL does not include one. 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 You can also use a shorthand for localhost
@@ -249,17 +251,6 @@ output_options.add_argument(
default=OUTPUT_OPTIONS_DEFAULT, default=OUTPUT_OPTIONS_DEFAULT,
) )
) )
output_options.add_argument(
'--verbose', '-v',
dest='output_options',
action='store_const',
const=''.join(OUTPUT_OPTIONS),
help="""
Print the whole request as well as the response. Shortcut for --print={0}.
"""
.format(''.join(OUTPUT_OPTIONS))
)
output_options.add_argument( output_options.add_argument(
'--headers', '-h', '--headers', '-h',
dest='output_options', dest='output_options',
@@ -283,6 +274,42 @@ output_options.add_argument(
.format(OUT_RESP_BODY) .format(OUT_RESP_BODY)
) )
output_options.add_argument(
'--verbose', '-v',
dest='verbose',
action='store_true',
help="""
Verbose output. Print the whole request as well as the response. Also print
any intermediary requests/responses (such as redirects).
It's a shortcut for: --all --print={0}
"""
.format(''.join(OUTPUT_OPTIONS))
)
output_options.add_argument(
'--all',
default=False,
action='store_true',
help="""
By default, only the final request/response is shown. Use this flag to show
any intermediary requests/responses as well. Intermediary requests include
followed redirects (with --follow), the first unauthorized request when
Digest auth is used (--auth=digest), etc.
"""
)
output_options.add_argument(
'--history-print', '-P',
dest='output_options_history',
metavar='WHAT',
help="""
The same as --print, -p but applies only to intermediary requests/responses
(such as redirects) when their inclusion is enabled with --all. If this
options is not specified, then they are formatted the same way as the final
response.
"""
)
output_options.add_argument( output_options.add_argument(
'--stream', '-S', '--stream', '-S',
action='store_true', action='store_true',
@@ -307,8 +334,9 @@ output_options.add_argument(
dest='output_file', dest='output_file',
metavar='FILE', metavar='FILE',
help=""" help="""
Save output to FILE. If --download is set, then only the response body is Save output to FILE instead of stdout. If --download is also set, then only
saved to the file. Other parts of the HTTP exchange are printed to stderr. the response body is saved to FILE. Other parts of the HTTP exchange are
printed to stderr.
""" """
@@ -377,7 +405,6 @@ sessions.add_argument(
""" """
) )
####################################################################### #######################################################################
# Authentication # Authentication
####################################################################### #######################################################################
@@ -397,7 +424,7 @@ auth.add_argument(
_auth_plugins = plugin_manager.get_auth_plugins() _auth_plugins = plugin_manager.get_auth_plugins()
auth.add_argument( auth.add_argument(
'--auth-type', '--auth-type', '-A',
choices=[plugin.auth_type for plugin in _auth_plugins], choices=[plugin.auth_type for plugin in _auth_plugins],
default=_auth_plugins[0].auth_type, default=_auth_plugins[0].auth_type,
help=""" help="""
@@ -444,45 +471,21 @@ network.add_argument(
""" """
) )
network.add_argument( network.add_argument(
'--follow', '--follow', '-F',
default=False, default=False,
action='store_true', action='store_true',
help=""" help="""
Set this flag if full redirects are allowed (e.g. re-POST-ing of data at Follow 30x Location redirects.
new Location).
"""
)
network.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".
""" """
) )
network.add_argument( network.add_argument(
'--cert', '--max-redirects',
default=None, type=int,
type=readable_file_arg, default=30,
help=""" help="""
You can specify a local cert to use as client side SSL certificate. By default, requests have a limit of 30 redirects (works with --follow).
This file may either contain both private key and certificate or you may
specify --cert-key separately.
"""
)
network.add_argument(
'--cert-key',
default=None,
type=readable_file_arg,
help="""
The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key.
""" """
) )
@@ -516,6 +519,57 @@ network.add_argument(
) )
#######################################################################
# SSL
#######################################################################
ssl = parser.add_argument_group(title='SSL')
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".
"""
)
ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
dest='ssl_version',
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
help="""
The desired protocol version to use. This will default to
SSL v2.3 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support. Available protocols
may vary depending on OpenSSL installation (only the supported ones
are shown here).
"""
)
ssl.add_argument(
'--cert',
default=None,
type=readable_file_arg,
help="""
You can specify a local cert to use as client side SSL certificate.
This file may either contain both private key and certificate or you may
specify --cert-key separately.
"""
)
ssl.add_argument(
'--cert-key',
default=None,
type=readable_file_arg,
help="""
The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key.
"""
)
####################################################################### #######################################################################
# Troubleshooting # Troubleshooting
####################################################################### #######################################################################
@@ -523,7 +577,7 @@ network.add_argument(
troubleshooting = parser.add_argument_group(title='Troubleshooting') troubleshooting = parser.add_argument_group(title='Troubleshooting')
troubleshooting.add_argument( troubleshooting.add_argument(
'--ignore-stdin', '--ignore-stdin', '-I',
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
@@ -554,7 +608,15 @@ troubleshooting.add_argument(
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
Prints exception traceback should one occur. Prints the exception traceback should one occur.
"""
)
troubleshooting.add_argument(
'--default-scheme',
default="http",
help="""
The default scheme to use if not specified in the URL.
""" """
) )
@@ -563,8 +625,8 @@ troubleshooting.add_argument(
action='store_true', action='store_true',
default=False, default=False,
help=""" help="""
Prints exception traceback should one occur, and also other information Prints the exception traceback should one occur, as well as other
that is useful for debugging HTTPie itself and for reporting bugs. information useful for debugging HTTPie itself and for reporting bugs.
""" """
) )

View File

@@ -1,18 +1,19 @@
import json import json
import sys import sys
from pprint import pformat
import requests import requests
from requests.adapters import HTTPAdapter
from requests.packages import urllib3 from requests.packages import urllib3
from httpie import sessions from httpie import sessions
from httpie import __version__ from httpie import __version__
from httpie.compat import str from httpie.compat import str
from httpie.input import SSL_VERSION_ARG_MAPPING
from httpie.plugins import plugin_manager from httpie.plugins import plugin_manager
from httpie.utils import repr_dict_nice
try: try:
# https://urllib3.readthedocs.org/en/latest/security.html # https://urllib3.readthedocs.io/en/latest/security.html
urllib3.disable_warnings() urllib3.disable_warnings()
except AttributeError: except AttributeError:
# In some rare cases, the user may have an old version of the requests # In some rare cases, the user may have an old version of the requests
@@ -22,13 +23,29 @@ except AttributeError:
pass pass
FORM = 'application/x-www-form-urlencoded; charset=utf-8' FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json' JSON_CONTENT_TYPE = 'application/json'
JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE)
DEFAULT_UA = 'HTTPie/%s' % __version__ DEFAULT_UA = 'HTTPie/%s' % __version__
def get_requests_session(): class HTTPieHTTPAdapter(HTTPAdapter):
def __init__(self, ssl_version=None, **kwargs):
self._ssl_version = ssl_version
super(HTTPieHTTPAdapter, self).__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_version'] = self._ssl_version
super(HTTPieHTTPAdapter, self).init_poolmanager(*args, **kwargs)
def get_requests_session(ssl_version):
requests_session = requests.Session() requests_session = requests.Session()
requests_session.mount(
'https://',
HTTPieHTTPAdapter(ssl_version=ssl_version)
)
for cls in plugin_manager.get_transport_plugins(): for cls in plugin_manager.get_transport_plugins():
transport_plugin = cls() transport_plugin = cls()
requests_session.mount(prefix=transport_plugin.prefix, requests_session.mount(prefix=transport_plugin.prefix,
@@ -39,7 +56,12 @@ def get_requests_session():
def get_response(args, config_dir): def get_response(args, config_dir):
"""Send the request and return a `request.Response`.""" """Send the request and return a `request.Response`."""
requests_session = get_requests_session() ssl_version = None
if args.ssl_version:
ssl_version = SSL_VERSION_ARG_MAPPING[args.ssl_version]
requests_session = get_requests_session(ssl_version)
requests_session.max_redirects = args.max_redirects
if not args.session and not args.session_read_only: if not args.session and not args.session_read_only:
kwargs = get_requests_kwargs(args) kwargs = get_requests_kwargs(args)
@@ -60,16 +82,26 @@ def get_response(args, config_dir):
def dump_request(kwargs): def dump_request(kwargs):
sys.stderr.write('\n>>> requests.request(**%s)\n\n' sys.stderr.write('\n>>> requests.request(**%s)\n\n'
% pformat(kwargs)) % repr_dict_nice(kwargs))
def encode_headers(headers): def finalize_headers(headers):
# This allows for unicode headers which is non-standard but practical. final_headers = {}
for name, value in headers.items():
if value is not None:
# >leading or trailing LWS MAY be removed without
# >changing the semantics of the field value"
# -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
# Also, requests raises `InvalidHeader` for leading spaces.
value = value.strip()
if isinstance(value, str):
# See: https://github.com/jkbrzt/httpie/issues/212 # See: https://github.com/jkbrzt/httpie/issues/212
return dict( value = value.encode('utf8')
(name, value.encode('utf8') if isinstance(value, str) else value)
for name, value in headers.items() final_headers[name] = value
) return final_headers
def get_default_headers(args): def get_default_headers(args):
@@ -78,16 +110,15 @@ def get_default_headers(args):
} }
auto_json = args.data and not args.form 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: 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): 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: elif args.form and not args.files:
# If sending files, `requests` will set # If sending files, `requests` will set
# the `Content-Type` for us. # the `Content-Type` for us.
default_headers['Content-Type'] = FORM default_headers['Content-Type'] = FORM_CONTENT_TYPE
return default_headers return default_headers
@@ -112,7 +143,7 @@ def get_requests_kwargs(args, base_headers=None):
if base_headers: if base_headers:
headers.update(base_headers) headers.update(base_headers)
headers.update(args.headers) headers.update(args.headers)
headers = encode_headers(headers) headers = finalize_headers(headers)
credentials = None credentials = None
if args.auth: if args.auth:

View File

@@ -14,10 +14,14 @@ is_windows = 'win32' in str(sys.platform).lower()
if is_py2: if is_py2:
# noinspection PyShadowingBuiltins
bytes = str bytes = str
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
str = unicode str = unicode
elif is_py3: elif is_py3:
# noinspection PyShadowingBuiltins
str = str str = str
# noinspection PyShadowingBuiltins
bytes = bytes bytes = bytes
@@ -32,7 +36,7 @@ try: # pragma: no cover
# noinspection PyCompatibility # noinspection PyCompatibility
from urllib.request import urlopen from urllib.request import urlopen
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
# noinspection PyCompatibility # noinspection PyCompatibility,PyUnresolvedReferences
from urllib2 import urlopen from urllib2 import urlopen
try: # pragma: no cover try: # pragma: no cover
@@ -40,10 +44,10 @@ try: # pragma: no cover
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
# Python 2.6 OrderedDict class, needed for headers, parameters, etc .### # Python 2.6 OrderedDict class, needed for headers, parameters, etc .###
# <https://pypi.python.org/pypi/ordereddict/1.1> # <https://pypi.python.org/pypi/ordereddict/1.1>
# noinspection PyCompatibility # noinspection PyCompatibility,PyUnresolvedReferences
from UserDict import DictMixin from UserDict import DictMixin
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins,PyCompatibility
class OrderedDict(dict, DictMixin): class OrderedDict(dict, DictMixin):
# Copyright (c) 2009 Raymond Hettinger # Copyright (c) 2009 Raymond Hettinger
# #
@@ -115,6 +119,7 @@ except ImportError: # pragma: no cover
if not self: if not self:
raise KeyError('dictionary is empty') raise KeyError('dictionary is empty')
if last: if last:
# noinspection PyUnresolvedReferences
key = reversed(self).next() key = reversed(self).next()
else: else:
key = iter(self).next() key = iter(self).next()

View File

@@ -6,11 +6,11 @@ from httpie import __version__
from httpie.compat import is_windows from httpie.compat import is_windows
DEFAULT_CONFIG_DIR = os.environ.get( DEFAULT_CONFIG_DIR = str(os.environ.get(
'HTTPIE_CONFIG_DIR', 'HTTPIE_CONFIG_DIR',
os.path.expanduser('~/.httpie') if not is_windows else os.path.expanduser('~/.httpie') if not is_windows else
os.path.expandvars(r'%APPDATA%\\httpie') os.path.expandvars(r'%APPDATA%\\httpie')
) ))
class BaseConfigDict(dict): class BaseConfigDict(dict):
@@ -84,7 +84,6 @@ class Config(BaseConfigDict):
about = 'HTTPie configuration file' about = 'HTTPie configuration file'
DEFAULTS = { DEFAULTS = {
'implicit_content_type': 'json',
'default_options': [] 'default_options': []
} }
@@ -93,5 +92,21 @@ class Config(BaseConfigDict):
self.update(self.DEFAULTS) self.update(self.DEFAULTS)
self.directory = directory self.directory = directory
def load(self):
super(Config, self).load()
self._migrate_implicit_content_type()
def _get_path(self): def _get_path(self):
return os.path.join(self.directory, self.name + '.json') return os.path.join(self.directory, self.name + '.json')
def _migrate_implicit_content_type(self):
"""Migrate the removed implicit_content_type config option"""
try:
implicit_content_type = self.pop('implicit_content_type')
except KeyError:
pass
else:
if implicit_content_type == 'form':
self['default_options'].insert(0, '--form')
self.save()
self.load()

View File

@@ -3,6 +3,8 @@ import sys
from httpie.compat import is_windows from httpie.compat import is_windows
from httpie.config import DEFAULT_CONFIG_DIR, Config from httpie.config import DEFAULT_CONFIG_DIR, Config
from httpie.utils import repr_dict_nice
class Environment(object): class Environment(object):
""" """
@@ -82,3 +84,17 @@ class Environment(object):
else: else:
self._config.load() self._config.load()
return self._config return self._config
def __str__(self):
defaults = dict(type(self).__dict__)
actual = dict(defaults)
actual.update(self.__dict__)
actual['config'] = self.config
return repr_dict_nice(dict(
(key, value)
for key, value in actual.items()
if not key.startswith('_'))
)
def __repr__(self):
return '<{0} {1}>'.format(type(self).__name__, str(self))

View File

@@ -12,6 +12,7 @@ Invocation flow:
""" """
import sys import sys
import errno import errno
import platform
import requests import requests
from requests import __version__ as requests_version from requests import __version__ as requests_version
@@ -20,12 +21,13 @@ from pygments import __version__ as pygments_version
from httpie import __version__ as httpie_version, ExitStatus from httpie import __version__ as httpie_version, ExitStatus
from httpie.compat import str, bytes, is_py3 from httpie.compat import str, bytes, is_py3
from httpie.client import get_response from httpie.client import get_response
from httpie.downloads import Download from httpie.downloads import Downloader
from httpie.context import Environment from httpie.context import Environment
from httpie.plugins import plugin_manager from httpie.plugins import plugin_manager
from httpie.output.streams import ( from httpie.output.streams import (
build_output_stream, build_output_stream,
write, write_with_colors_win_py3 write_stream,
write_stream_with_colors_win_py3
) )
@@ -47,11 +49,14 @@ def get_exit_status(http_status, follow=False):
def print_debug_info(env): def print_debug_info(env):
env.stderr.writelines([ env.stderr.writelines([
'HTTPie %s\n' % httpie_version, 'HTTPie %s\n' % httpie_version,
'HTTPie data: %s\n' % env.config.directory,
'Requests %s\n' % requests_version, 'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version, 'Pygments %s\n' % pygments_version,
'Python %s %s\n' % (sys.version, sys.platform) 'Python %s\n%s\n' % (sys.version, sys.executable),
'%s %s' % (platform.system(), platform.release()),
]) ])
env.stderr.write('\n\n')
env.stderr.write(repr(env))
env.stderr.write('\n')
def decode_args(args, stdin_encoding): def decode_args(args, stdin_encoding):
@@ -67,8 +72,109 @@ def decode_args(args, stdin_encoding):
] ]
def main(args=sys.argv[1:], env=Environment(), error=None): def program(args, env, log_error):
"""Run the main program and write the output to ``env.stdout``. """
The main program without error handling
:param args: parsed args (argparse.Namespace)
:type env: Environment
:param log_error: error log function
:return: status code
"""
exit_status = ExitStatus.OK
downloader = None
show_traceback = args.debug or args.traceback
try:
if args.download:
args.follow = True # --download implies --follow.
downloader = Downloader(
output_file=args.output_file,
progress_file=env.stderr,
resume=args.download_resume
)
downloader.pre_request(args.headers)
final_response = get_response(args, config_dir=env.config.directory)
if args.all:
responses = final_response.history + [final_response]
else:
responses = [final_response]
for response in responses:
if args.check_status or downloader:
exit_status = get_exit_status(
http_status=response.status_code,
follow=args.follow
)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
log_error(
'HTTP %s %s', response.raw.status, response.raw.reason,
level='warning'
)
write_stream_kwargs = {
'stream': build_output_stream(
args=args,
env=env,
request=response.request,
response=response,
output_options=(
args.output_options
if response is final_response
else args.output_options_history
)
),
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_stream_with_colors_win_py3(**write_stream_kwargs)
else:
write_stream(**write_stream_kwargs)
except IOError as e:
if not show_traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
if downloader and exit_status == ExitStatus.OK:
# Last response body download.
download_stream, download_to = downloader.start(final_response)
write_stream(
stream=download_stream,
outfile=download_to,
flush=False,
)
downloader.finish()
if downloader.interrupted:
exit_status = ExitStatus.ERROR
log_error('Incomplete download: size=%d; downloaded=%d' % (
downloader.status.total_size,
downloader.status.downloaded
))
return exit_status
finally:
if downloader and not downloader.finished:
downloader.failed()
if (not isinstance(args, list) and args.output_file and
args.output_file_specified):
args.output_file.close()
def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
"""
The main function.
Pre-process args, handle some special types of invocations,
and run the main program with error handling.
Return exit status code. Return exit status code.
@@ -76,127 +182,79 @@ def main(args=sys.argv[1:], env=Environment(), error=None):
args = decode_args(args, env.stdin_encoding) args = decode_args(args, env.stdin_encoding)
plugin_manager.load_installed_plugins() plugin_manager.load_installed_plugins()
def log_error(msg, *args, **kwargs):
msg = msg % args
level = kwargs.get('level', 'error')
assert level in ['error', 'warning']
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
from httpie.cli import parser from httpie.cli import parser
if env.config.default_options: if env.config.default_options:
args = env.config.default_options + args args = env.config.default_options + args
def _error(msg, *args, **kwargs): if custom_log_error:
msg = msg % args log_error = custom_log_error
level = kwargs.get('level', 'error')
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
if error is None: include_debug_info = '--debug' in args
error = _error include_traceback = include_debug_info or '--traceback' in args
debug = '--debug' in args if include_debug_info:
traceback = debug or '--traceback' in args
exit_status = ExitStatus.OK
if debug:
print_debug_info(env) print_debug_info(env)
if args == ['--debug']: if args == ['--debug']:
return exit_status return ExitStatus.OK
download = None exit_status = ExitStatus.OK
try: try:
args = parser.parse_args(args=args, env=env) parsed_args = parser.parse_args(args=args, env=env)
if args.download:
args.follow = True # --download implies --follow.
download = Download(
output_file=args.output_file,
progress_file=env.stderr,
resume=args.download_resume
)
download.pre_request(args.headers)
response = get_response(args, config_dir=env.config.directory)
if args.check_status or download:
exit_status = get_exit_status(
http_status=response.status_code,
follow=args.follow
)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
error('HTTP %s %s',
response.raw.status,
response.raw.reason,
level='warning')
write_kwargs = {
'stream': build_output_stream(
args, env, response.request, response),
# This will in fact be `stderr` with `--download`
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_with_colors_win_py3(**write_kwargs)
else:
write(**write_kwargs)
if download and exit_status == ExitStatus.OK:
# Response body download.
download_stream, download_to = download.start(response)
write(
stream=download_stream,
outfile=download_to,
flush=False,
)
download.finish()
if download.interrupted:
exit_status = ExitStatus.ERROR
error('Incomplete download: size=%d; downloaded=%d' % (
download.status.total_size,
download.status.downloaded
))
except IOError as e:
if not traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
except KeyboardInterrupt: except KeyboardInterrupt:
if traceback:
raise
env.stderr.write('\n') env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
except SystemExit as e: except SystemExit as e:
if e.code != ExitStatus.OK: if e.code != ExitStatus.OK:
if traceback:
raise
env.stderr.write('\n') env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
else:
try:
exit_status = program(
args=parsed_args,
env=env,
log_error=log_error,
)
except KeyboardInterrupt:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except SystemExit as e:
if e.code != ExitStatus.OK:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
except requests.Timeout: except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT exit_status = ExitStatus.ERROR_TIMEOUT
error('Request timed out (%ss).', args.timeout) log_error('Request timed out (%ss).', parsed_args.timeout)
except requests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
log_error('Too many redirects (--max-redirects=%s).',
parsed_args.max_redirects)
except Exception as e: except Exception as e:
# TODO: Better distinction between expected and unexpected errors. # TODO: Further distinction between expected and unexpected errors.
# Network errors vs. bugs, etc.
if traceback:
raise
msg = str(e) msg = str(e)
if hasattr(e, 'request'): if hasattr(e, 'request'):
request = e.request request = e.request
if hasattr(request, 'url'): if hasattr(request, 'url'):
msg += ' while doing %s request to URL: %s' % ( msg += ' while doing %s request to URL: %s' % (
request.method, request.url) request.method, request.url)
error('%s: %s', type(e).__name__, msg) log_error('%s: %s', type(e).__name__, msg)
if include_traceback:
raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
finally:
if download and not download.finished:
download.failed()
return exit_status return exit_status

View File

@@ -7,6 +7,7 @@ from __future__ import division
import os import os
import re import re
import sys import sys
import errno
import mimetypes import mimetypes
import threading import threading
from time import sleep, time from time import sleep, time
@@ -77,15 +78,15 @@ def parse_content_range(content_range, resumed_from):
# last-byte-pos value, is invalid. The recipient of an invalid # last-byte-pos value, is invalid. The recipient of an invalid
# byte-content-range- spec MUST ignore it and any content # byte-content-range- spec MUST ignore it and any content
# transferred along with it." # transferred along with it."
if (first_byte_pos >= last_byte_pos if (first_byte_pos >= last_byte_pos or
or (instance_length is not None (instance_length is not None and
and instance_length <= last_byte_pos)): instance_length <= last_byte_pos)):
raise ContentRangeError( raise ContentRangeError(
'Invalid Content-Range returned: %r' % content_range) 'Invalid Content-Range returned: %r' % content_range)
if (first_byte_pos != resumed_from if (first_byte_pos != resumed_from or
or (instance_length is not None (instance_length is not None and
and last_byte_pos + 1 != instance_length)): last_byte_pos + 1 != instance_length)):
# Not what we asked for. # Not what we asked for.
raise ContentRangeError( raise ContentRangeError(
'Unexpected Content-Range returned (%r)' 'Unexpected Content-Range returned (%r)'
@@ -135,16 +136,51 @@ def filename_from_url(url, content_type):
return fn return fn
def trim_filename(filename, max_len):
if len(filename) > max_len:
trim_by = len(filename) - max_len
name, ext = os.path.splitext(filename)
if trim_by >= len(name):
filename = filename[:-trim_by]
else:
filename = name[:-trim_by] + ext
return filename
def get_filename_max_length(directory):
max_len = 255
try:
pathconf = os.pathconf
except AttributeError:
pass # non-posix
else:
try:
max_len = pathconf(directory, 'PC_NAME_MAX')
except OSError as e:
if e.errno != errno.EINVAL:
raise
return max_len
def trim_filename_if_needed(filename, directory='.', extra=0):
max_len = get_filename_max_length(directory) - extra
if len(filename) > max_len:
filename = trim_filename(filename, max_len)
return filename
def get_unique_filename(filename, exists=os.path.exists): def get_unique_filename(filename, exists=os.path.exists):
attempt = 0 attempt = 0
while True: while True:
suffix = '-' + str(attempt) if attempt > 0 else '' suffix = '-' + str(attempt) if attempt > 0 else ''
if not exists(filename + suffix): try_filename = trim_filename_if_needed(filename, extra=len(suffix))
return filename + suffix try_filename += suffix
if not exists(try_filename):
return try_filename
attempt += 1 attempt += 1
class Download(object): class Downloader(object):
def __init__(self, output_file=None, def __init__(self, output_file=None,
resume=False, progress_file=sys.stderr): resume=False, progress_file=sys.stderr):
@@ -178,8 +214,8 @@ class Download(object):
:type request_headers: dict :type request_headers: dict
""" """
# Disable content encoding so that we can resume, etc. # Ask the server not to encode the content so that we can resume, etc.
request_headers['Accept-Encoding'] = None request_headers['Accept-Encoding'] = 'identity'
if self._resume: if self._resume:
bytes_have = os.path.getsize(self._output_file.name) bytes_have = os.path.getsize(self._output_file.name)
if bytes_have: if bytes_have:
@@ -201,6 +237,8 @@ class Download(object):
""" """
assert not self.status.time_started assert not self.status.time_started
# FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/jkbrzt/httpie/issues/423>
try: try:
total_size = int(response.headers['Content-Length']) total_size = int(response.headers['Content-Length'])
except (KeyError, ValueError, TypeError): except (KeyError, ValueError, TypeError):
@@ -270,9 +308,9 @@ class Download(object):
@property @property
def interrupted(self): def interrupted(self):
return ( return (
self.finished self.finished and
and self.status.total_size self.status.total_size and
and self.status.total_size != self.status.downloaded self.status.total_size != self.status.downloaded
) )
def chunk_downloaded(self, chunk): def chunk_downloaded(self, chunk):
@@ -299,7 +337,6 @@ class Status(object):
def started(self, resumed_from=0, total_size=None): def started(self, resumed_from=0, total_size=None):
assert self.time_started is None assert self.time_started is None
if total_size is not None:
self.total_size = total_size self.total_size = total_size
self.downloaded = self.resumed_from = resumed_from self.downloaded = self.resumed_from = resumed_from
self.time_started = time() self.time_started = time()
@@ -362,8 +399,8 @@ class ProgressReporterThread(threading.Thread):
if now - self._prev_time >= self._update_interval: if now - self._prev_time >= self._update_interval:
downloaded = self.status.downloaded downloaded = self.status.downloaded
try: try:
speed = ((downloaded - self._prev_bytes) speed = ((downloaded - self._prev_bytes) /
/ (now - self._prev_time)) (now - self._prev_time))
except ZeroDivisionError: except ZeroDivisionError:
speed = 0 speed = 0
@@ -397,11 +434,11 @@ class ProgressReporterThread(threading.Thread):
self._prev_bytes = downloaded self._prev_bytes = downloaded
self.output.write( self.output.write(
CLEAR_LINE CLEAR_LINE +
+ ' ' ' ' +
+ SPINNER[self._spinner_pos] SPINNER[self._spinner_pos] +
+ ' ' ' ' +
+ self._status_line self._status_line
) )
self.output.flush() self.output.flush()
@@ -410,8 +447,8 @@ class ProgressReporterThread(threading.Thread):
else 0) else 0)
def sum_up(self): def sum_up(self):
actually_downloaded = (self.status.downloaded actually_downloaded = (
- self.status.resumed_from) self.status.downloaded - self.status.resumed_from)
time_taken = self.status.time_finished - self.status.time_started time_taken = self.status.time_finished - self.status.time_started
self.output.write(CLEAR_LINE) self.output.write(CLEAR_LINE)
@@ -426,8 +463,8 @@ class ProgressReporterThread(threading.Thread):
self.output.write(SUMMARY.format( self.output.write(SUMMARY.format(
downloaded=humanize_bytes(actually_downloaded), downloaded=humanize_bytes(actually_downloaded),
total=(self.status.total_size total=(self.status.total_size and
and humanize_bytes(self.status.total_size)), humanize_bytes(self.status.total_size)),
speed=humanize_bytes(speed), speed=humanize_bytes(speed),
time=time_taken, time=time_taken,
)) ))

View File

@@ -2,6 +2,7 @@
""" """
import os import os
import ssl
import sys import sys
import re import re
import errno import errno
@@ -27,12 +28,11 @@ URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
HTTP_POST = 'POST' HTTP_POST = 'POST'
HTTP_GET = 'GET' HTTP_GET = 'GET'
HTTP = 'http://'
HTTPS = 'https://'
# Various separators used in args # Various separators used in args
SEP_HEADERS = ':' SEP_HEADERS = ':'
SEP_HEADERS_EMPTY = ';'
SEP_CREDENTIALS = ':' SEP_CREDENTIALS = ':'
SEP_PROXY = ':' SEP_PROXY = ':'
SEP_DATA = '=' SEP_DATA = '='
@@ -66,6 +66,7 @@ SEP_GROUP_RAW_JSON_ITEMS = frozenset([
# Separators allowed in ITEM arguments # Separators allowed in ITEM arguments
SEP_GROUP_ALL_ITEMS = frozenset([ SEP_GROUP_ALL_ITEMS = frozenset([
SEP_HEADERS, SEP_HEADERS,
SEP_HEADERS_EMPTY,
SEP_QUERY, SEP_QUERY,
SEP_DATA, SEP_DATA,
SEP_DATA_RAW_JSON, SEP_DATA_RAW_JSON,
@@ -103,7 +104,21 @@ OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
class Parser(ArgumentParser): SSL_VERSION_ARG_MAPPING = {
'ssl2.3': 'PROTOCOL_SSLv23',
'ssl3': 'PROTOCOL_SSLv3',
'tls1': 'PROTOCOL_TLSv1',
'tls1.1': 'PROTOCOL_TLSv1_1',
'tls1.2': 'PROTOCOL_TLSv1_2',
}
SSL_VERSION_ARG_MAPPING = dict(
(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):
"""Adds additional logic to `argparse.ArgumentParser`. """Adds additional logic to `argparse.ArgumentParser`.
Handles all input (CLI args, file args, stdin), applies defaults, Handles all input (CLI args, file args, stdin), applies defaults,
@@ -113,13 +128,13 @@ class Parser(ArgumentParser):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['add_help'] = False kwargs['add_help'] = False
super(Parser, self).__init__(*args, **kwargs) super(HTTPieArgumentParser, self).__init__(*args, **kwargs)
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None): def parse_args(self, env, args=None, namespace=None):
self.env = env self.env = env
self.args, no_options = super(Parser, self)\ self.args, no_options = super(HTTPieArgumentParser, self)\
.parse_known_args(args, namespace) .parse_known_args(args, namespace)
if self.args.debug: if self.args.debug:
@@ -127,7 +142,6 @@ class Parser(ArgumentParser):
# Arguments processing and environment setup. # Arguments processing and environment setup.
self._apply_no_options(no_options) self._apply_no_options(no_options)
self._apply_config()
self._validate_download_options() self._validate_download_options()
self._setup_standard_streams() self._setup_standard_streams()
self._process_output_options() self._process_output_options()
@@ -137,7 +151,7 @@ class Parser(ArgumentParser):
if not self.args.ignore_stdin and not env.stdin_isatty: if not self.args.ignore_stdin and not env.stdin_isatty:
self._body_from_file(self.env.stdin) self._body_from_file(self.env.stdin)
if not URL_SCHEME_RE.match(self.args.url): 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) # See if we're using curl style shorthand for localhost (:3000/foo)
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url) shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
@@ -164,19 +178,17 @@ class Parser(ArgumentParser):
}.get(file, file) }.get(file, file)
if not hasattr(file, 'buffer') and isinstance(message, str): if not hasattr(file, 'buffer') and isinstance(message, str):
message = message.encode(self.env.stdout_encoding) message = message.encode(self.env.stdout_encoding)
super(Parser, self)._print_message(message, file) super(HTTPieArgumentParser, self)._print_message(message, file)
def _setup_standard_streams(self): def _setup_standard_streams(self):
""" """
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed. Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
""" """
if not self.env.stdout_isatty and self.args.output_file: self.args.output_file_specified = bool(self.args.output_file)
self.error('Cannot use --output, -o with redirected output.')
if self.args.download: if self.args.download:
# FIXME: Come up with a cleaner solution. # FIXME: Come up with a cleaner solution.
if not self.env.stdout_isatty: if not self.args.output_file and not self.env.stdout_isatty:
# Use stdout as the download output file. # Use stdout as the download output file.
self.args.output_file = self.env.stdout self.args.output_file = self.env.stdout
# With `--download`, we write everything that would normally go to # With `--download`, we write everything that would normally go to
@@ -201,11 +213,6 @@ class Parser(ArgumentParser):
self.env.stdout = self.args.output_file self.env.stdout = self.args.output_file
self.env.stdout_isatty = False self.env.stdout_isatty = False
def _apply_config(self):
if (not self.args.json
and self.env.config.implicit_content_type == 'form'):
self.args.form = True
def _process_auth(self): def _process_auth(self):
""" """
If only a username provided via --auth, then ask for a password. If only a username provided via --auth, then ask for a password.
@@ -302,8 +309,9 @@ class Parser(ArgumentParser):
self.args.url = self.args.method self.args.url = self.args.method
# Infer the method # Infer the method
has_data = ( has_data = (
(not self.args.ignore_stdin and not self.env.stdin_isatty) (not self.args.ignore_stdin and
or any(item.sep in SEP_GROUP_DATA_ITEMS not self.env.stdin_isatty) or
any(item.sep in SEP_GROUP_DATA_ITEMS
for item in self.args.items) for item in self.args.items)
) )
self.args.method = HTTP_POST if has_data else HTTP_GET self.args.method = HTTP_POST if has_data else HTTP_GET
@@ -336,17 +344,14 @@ class Parser(ArgumentParser):
'Invalid file fields (perhaps you meant --form?): %s' 'Invalid file fields (perhaps you meant --form?): %s'
% ','.join(file_fields)) % ','.join(file_fields))
fn, fd = self.args.files[''] fn, fd, ct = self.args.files['']
self.args.files = {} self.args.files = {}
self._body_from_file(fd) self._body_from_file(fd)
if 'Content-Type' not in self.args.headers: if 'Content-Type' not in self.args.headers:
mime, encoding = mimetypes.guess_type(fn, strict=False) content_type = get_content_type(fn)
if mime: if content_type:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
self.args.headers['Content-Type'] = content_type self.args.headers['Content-Type'] = content_type
def _process_output_options(self): def _process_output_options(self):
@@ -355,18 +360,32 @@ class Parser(ArgumentParser):
The default output options are stdout-type-sensitive. The default output options are stdout-type-sensitive.
""" """
if not self.args.output_options: def check_options(value, option):
unknown = set(value) - OUTPUT_OPTIONS
if unknown:
self.error('Unknown output options: {0}={1}'.format(
option,
','.join(unknown)
))
if self.args.verbose:
self.args.all = True
if self.args.output_options is None:
if self.args.verbose:
self.args.output_options = ''.join(OUTPUT_OPTIONS)
else:
self.args.output_options = ( self.args.output_options = (
OUTPUT_OPTIONS_DEFAULT OUTPUT_OPTIONS_DEFAULT
if self.env.stdout_isatty if self.env.stdout_isatty
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
) )
unknown_output_options = set(self.args.output_options) - OUTPUT_OPTIONS if self.args.output_options_history is None:
if unknown_output_options: self.args.output_options_history = self.args.output_options
self.error(
'Unknown output options: %s' % ','.join(unknown_output_options) check_options(self.args.output_options, '--print')
) check_options(self.args.output_options_history, '--history-print')
if self.args.download and OUT_RESP_BODY in self.args.output_options: if self.args.download and OUT_RESP_BODY in self.args.output_options:
# Response body is always downloaded with --download and it goes # Response body is always downloaded with --download and it goes
@@ -421,8 +440,8 @@ class SessionNameValidator(object):
def __call__(self, value): def __call__(self, value):
# Session name can be a path or just a name. # Session name can be a path or just a name.
if (os.path.sep not in value if (os.path.sep not in value and
and not VALID_SESSION_NAME_PATTERN.search(value)): not VALID_SESSION_NAME_PATTERN.search(value)):
raise ArgumentError(None, self.error_message) raise ArgumentError(None, self.error_message)
return value return value
@@ -609,6 +628,21 @@ RequestItems = namedtuple('RequestItems',
['headers', 'data', 'files', 'params']) ['headers', 'data', 'files', 'params'])
def get_content_type(filename):
"""
Return the content type for ``filename`` in format appropriate
for Content-Type headers, or ``None`` if the file type is unknown
to ``mimetypes``.
"""
mime, encoding = mimetypes.guess_type(filename, strict=False)
if mime:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
return content_type
def parse_items(items, def parse_items(items,
headers_class=CaseInsensitiveDict, headers_class=CaseInsensitiveDict,
data_class=OrderedDict, data_class=OrderedDict,
@@ -622,11 +656,20 @@ def parse_items(items,
data = [] data = []
files = [] files = []
params = [] params = []
for item in items: for item in items:
value = item.value value = item.value
if item.sep == SEP_HEADERS: 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 target = headers
elif item.sep == SEP_QUERY: elif item.sep == SEP_QUERY:
target = params target = params
@@ -634,7 +677,8 @@ def parse_items(items,
try: try:
with open(os.path.expanduser(value), 'rb') as f: with open(os.path.expanduser(value), 'rb') as f:
value = (os.path.basename(value), value = (os.path.basename(value),
BytesIO(f.read())) BytesIO(f.read()),
get_content_type(value))
except IOError as e: except IOError as e:
raise ParseError('"%s": %s' % (item.orig, e)) raise ParseError('"%s": %s' % (item.orig, e))
target = files target = files

View File

@@ -1,3 +1,6 @@
from __future__ import absolute_import
import json
import pygments.lexer import pygments.lexer
import pygments.token import pygments.token
import pygments.styles import pygments.styles
@@ -5,16 +8,22 @@ import pygments.lexers
import pygments.style import pygments.style
from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.lexers.special import TextLexer
from pygments.util import ClassNotFound from pygments.util import ClassNotFound
from httpie.compat import is_windows
from httpie.plugins import FormatterPlugin from httpie.plugins import FormatterPlugin
# Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there.
AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys()) AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys())
AVAILABLE_STYLES.add('solarized') AVAILABLE_STYLES.add('solarized')
DEFAULT_STYLE = 'monokai'
if is_windows:
# Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there
DEFAULT_STYLE = 'fruity'
else:
DEFAULT_STYLE = 'solarized'
class ColorFormatter(FormatterPlugin): class ColorFormatter(FormatterPlugin):
@@ -27,14 +36,15 @@ class ColorFormatter(FormatterPlugin):
""" """
group_name = 'colors' group_name = 'colors'
def __init__(self, env, color_scheme=DEFAULT_STYLE, **kwargs): def __init__(self, env, explicit_json=False,
color_scheme=DEFAULT_STYLE, **kwargs):
super(ColorFormatter, self).__init__(**kwargs) super(ColorFormatter, self).__init__(**kwargs)
if not env.colors: if not env.colors:
self.enabled = False self.enabled = False
return return
# Cache to speed things up when we process streamed body by line. # --json, -j
self.lexer_cache = {} self.explicit_json = explicit_json
try: try:
style_class = pygments.styles.get_style_by_name(color_scheme) style_class = pygments.styles.get_style_by_name(color_scheme)
@@ -51,34 +61,40 @@ class ColorFormatter(FormatterPlugin):
return pygments.highlight(headers, HTTPLexer(), self.formatter).strip() return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
def format_body(self, body, mime): def format_body(self, body, mime):
lexer = self.get_lexer(mime) lexer = self.get_lexer(mime, body)
if lexer: if lexer:
body = pygments.highlight(body, lexer, self.formatter) body = pygments.highlight(body, lexer, self.formatter)
return body.strip() return body.strip()
def get_lexer(self, mime): def get_lexer(self, mime, body):
if mime in self.lexer_cache: return get_lexer(
return self.lexer_cache[mime] mime=mime,
self.lexer_cache[mime] = get_lexer(mime) explicit_json=self.explicit_json,
return self.lexer_cache[mime] body=body,
)
def get_lexer(mime): def get_lexer(mime, explicit_json=False, body=''):
# Build candidate mime type and lexer names.
mime_types, lexer_names = [mime], [] mime_types, lexer_names = [mime], []
type_, subtype = mime.split('/') type_, subtype = mime.split('/', 1)
if '+' not in subtype: if '+' not in subtype:
lexer_names.append(subtype) lexer_names.append(subtype)
else: else:
subtype_name, subtype_suffix = subtype.split('+') subtype_name, subtype_suffix = subtype.split('+', 1)
lexer_names.extend([subtype_name, subtype_suffix]) lexer_names.extend([subtype_name, subtype_suffix])
mime_types.extend([ mime_types.extend([
'%s/%s' % (type_, subtype_name), '%s/%s' % (type_, subtype_name),
'%s/%s' % (type_, subtype_suffix) '%s/%s' % (type_, subtype_suffix)
]) ])
# as a last resort, if no lexer feels responsible, and
# As a last resort, if no lexer feels responsible, and
# the subtype contains 'json', take the JSON lexer # the subtype contains 'json', take the JSON lexer
if 'json' in subtype: if 'json' in subtype:
lexer_names.append('json') lexer_names.append('json')
# Try to resolve the right lexer.
lexer = None lexer = None
for mime_type in mime_types: for mime_type in mime_types:
try: try:
@@ -92,6 +108,16 @@ def get_lexer(mime):
lexer = pygments.lexers.get_lexer_by_name(name) lexer = pygments.lexers.get_lexer_by_name(name)
except ClassNotFound: except ClassNotFound:
pass pass
if explicit_json and body and (not lexer or isinstance(lexer, TextLexer)):
# JSON response with an incorrect Content-Type?
try:
json.loads(body) # FIXME: the body also gets parsed in json.py
except ValueError:
pass # Nope
else:
lexer = pygments.lexers.get_lexer_by_name('json')
return lexer return lexer
@@ -143,8 +169,6 @@ class HTTPLexer(pygments.lexer.RegexLexer):
} }
# TODO: As Solarized is not the default theme any longer, it should be removed
# or bundled directly with Pygments so that we don't need to support it.
class Solarized256Style(pygments.style.Style): class Solarized256Style(pygments.style.Style):
""" """
solarized256 solarized256

View File

@@ -10,17 +10,24 @@ DEFAULT_INDENT = 4
class JSONFormatter(FormatterPlugin): class JSONFormatter(FormatterPlugin):
def format_body(self, body, mime): def format_body(self, body, mime):
if 'json' in mime: maybe_json = [
'json',
'javascript',
'text',
]
if (self.kwargs['explicit_json'] or
any(token in mime for token in maybe_json)):
try: try:
obj = json.loads(body) obj = json.loads(body)
except ValueError: except ValueError:
# Invalid JSON, ignore. pass # Invalid JSON, ignore.
pass
else: else:
# Indent, sort keys by name, and avoid # Indent, sort keys by name, and avoid
# unicode escapes to improve readability. # unicode escapes to improve readability.
body = json.dumps(obj, body = json.dumps(
obj=obj,
sort_keys=True, sort_keys=True,
ensure_ascii=False, ensure_ascii=False,
indent=DEFAULT_INDENT) indent=DEFAULT_INDENT
)
return body return body

View File

@@ -1,61 +0,0 @@
from __future__ import absolute_import
import re
from xml.etree import ElementTree
from httpie.plugins import FormatterPlugin
DECLARATION_RE = re.compile('<\?xml[^\n]+?\?>', flags=re.I)
DOCTYPE_RE = re.compile('<!DOCTYPE[^\n]+?>', flags=re.I)
DEFAULT_INDENT = 4
def indent(elem, indent_text=' ' * DEFAULT_INDENT):
"""
In-place prettyprint formatter
C.f. http://effbot.org/zone/element-lib.htm#prettyprint
"""
def _indent(elem, level=0):
i = "\n" + level * indent_text
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + indent_text
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
_indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
return _indent(elem)
class XMLFormatter(FormatterPlugin):
# TODO: tests
def format_body(self, body, mime):
if 'xml' in mime:
# FIXME: orig NS names get forgotten during the conversion, etc.
try:
root = ElementTree.fromstring(body.encode('utf8'))
except ElementTree.ParseError:
# Ignore invalid XML errors (skips attempting to pretty print)
pass
else:
indent(root)
# Use the original declaration
declaration = DECLARATION_RE.match(body)
doctype = DOCTYPE_RE.match(body)
body = ElementTree.tostring(root, encoding='utf-8')\
.decode('utf8')
if doctype:
body = '%s\n%s' % (doctype.group(0), body)
if declaration:
body = '%s\n%s' % (declaration.group(0), body)
return body

View File

@@ -24,7 +24,7 @@ class BinarySuppressedError(Exception):
message = BINARY_SUPPRESSED_NOTICE message = BINARY_SUPPRESSED_NOTICE
def write(stream, outfile, flush): def write_stream(stream, outfile, flush):
"""Write the output stream.""" """Write the output stream."""
try: try:
# Writing bytes so we use the buffer interface (Python 3). # Writing bytes so we use the buffer interface (Python 3).
@@ -38,7 +38,7 @@ def write(stream, outfile, flush):
outfile.flush() outfile.flush()
def write_with_colors_win_py3(stream, outfile, flush): def write_stream_with_colors_win_py3(stream, outfile, flush):
"""Like `write`, but colorized chunks are written as text """Like `write`, but colorized chunks are written as text
directly to `outfile` to ensure it gets processed by colorama. directly to `outfile` to ensure it gets processed by colorama.
Applies only to Windows with Python 3 and colorized terminal output. Applies only to Windows with Python 3 and colorized terminal output.
@@ -55,15 +55,15 @@ def write_with_colors_win_py3(stream, outfile, flush):
outfile.flush() outfile.flush()
def build_output_stream(args, env, request, response): def build_output_stream(args, env, request, response, output_options):
"""Build and return a chain of iterators over the `request`-`response` """Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks. exchange each of which yields `bytes` chunks.
""" """
req_h = OUT_REQ_HEAD in args.output_options req_h = OUT_REQ_HEAD in output_options
req_b = OUT_REQ_BODY in args.output_options req_b = OUT_REQ_BODY in output_options
resp_h = OUT_RESP_HEAD in args.output_options resp_h = OUT_RESP_HEAD in output_options
resp_b = OUT_RESP_BODY in args.output_options resp_b = OUT_RESP_BODY in output_options
req = req_h or req_b req = req_h or req_b
resp = resp_h or resp_b resp = resp_h or resp_b
@@ -112,8 +112,12 @@ def get_stream_type(env, args):
PrettyStream if args.stream else BufferedPrettyStream, PrettyStream if args.stream else BufferedPrettyStream,
env=env, env=env,
conversion=Conversion(), conversion=Conversion(),
formatting=Formatting(env=env, groups=args.prettify, formatting=Formatting(
color_scheme=args.style), env=env,
groups=args.prettify,
color_scheme=args.style,
explicit_json=args.json,
),
) )
else: else:
Stream = partial(EncodedStream, env=env) Stream = partial(EncodedStream, env=env)

View File

@@ -11,7 +11,6 @@ from httpie.plugins.manager import PluginManager
from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin
from httpie.output.formatters.headers import HeadersFormatter from httpie.output.formatters.headers import HeadersFormatter
from httpie.output.formatters.json import JSONFormatter from httpie.output.formatters.json import JSONFormatter
from httpie.output.formatters.xml import XMLFormatter
from httpie.output.formatters.colors import ColorFormatter from httpie.output.formatters.colors import ColorFormatter
@@ -20,5 +19,4 @@ plugin_manager.register(BasicAuthPlugin,
DigestAuthPlugin) DigestAuthPlugin)
plugin_manager.register(HeadersFormatter, plugin_manager.register(HeadersFormatter,
JSONFormatter, JSONFormatter,
XMLFormatter,
ColorFormatter) ColorFormatter)

View File

@@ -5,6 +5,7 @@ import requests.auth
from httpie.plugins.base import AuthPlugin from httpie.plugins.base import AuthPlugin
# noinspection PyAbstractClass
class BuiltinAuthPlugin(AuthPlugin): class BuiltinAuthPlugin(AuthPlugin):
package_name = '(builtin)' package_name = '(builtin)'

View File

@@ -22,7 +22,7 @@ SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(requests_session, session_name, def get_response(requests_session, session_name,
config_dir, args, read_only=False): config_dir, args, read_only=False):
"""Like `client.get_response`, but applies permanent """Like `client.get_responses`, but applies permanent
aspects of the session to the request. aspects of the session to the request.
""" """
@@ -30,8 +30,8 @@ def get_response(requests_session, session_name,
if os.path.sep in session_name: if os.path.sep in session_name:
path = os.path.expanduser(session_name) path = os.path.expanduser(session_name)
else: else:
hostname = (args.headers.get('Host', None) hostname = (args.headers.get('Host', None) or
or urlsplit(args.url).netloc.split('@')[-1]) urlsplit(args.url).netloc.split('@')[-1])
if not hostname: if not hostname:
# HACK/FIXME: httpie-unixsocket's URLs have no hostname. # HACK/FIXME: httpie-unixsocket's URLs have no hostname.
hostname = 'localhost' hostname = 'localhost'
@@ -101,6 +101,10 @@ class Session(BaseConfigDict):
""" """
for name, value in request_headers.items(): for name, value in request_headers.items():
if value is None:
continue # Ignore explicitely unset headers
value = value.decode('utf8') value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'): if name == 'User-Agent' and value.startswith('HTTPie/'):
continue continue

View File

@@ -10,6 +10,22 @@ def load_json_preserve_order(s):
return json.loads(s, object_pairs_hook=OrderedDict) return json.loads(s, object_pairs_hook=OrderedDict)
def repr_dict_nice(d):
def prepare_dict(d):
for k, v in d.items():
if isinstance(v, dict):
v = dict(prepare_dict(v))
elif isinstance(v, bytes):
v = v.decode('utf8')
elif not isinstance(v, (int, str)):
v = repr(v)
yield k, v
return json.dumps(
dict(prepare_dict(d)),
indent=4, sort_keys=True,
)
def humanize_bytes(n, precision=2): def humanize_bytes(n, precision=2):
# Author: Doug Latornell # Author: Doug Latornell
# Licence: MIT # Licence: MIT

View File

@@ -1,3 +1,2 @@
[pytest] [pytest]
addopts = --tb=native
norecursedirs = tests/fixtures norecursedirs = tests/fixtures

View File

@@ -35,10 +35,11 @@ tests_require = [
install_requires = [ install_requires = [
'requests>=2.3.0', 'requests>=2.11.0',
'Pygments>=1.5' 'Pygments>=2.1.3'
] ]
# Conditional dependencies: # Conditional dependencies:
# sdist # sdist
@@ -56,7 +57,7 @@ if 'bdist_wheel' not in sys.argv:
# bdist_wheel # bdist_wheel
extras_require = { extras_require = {
# http://wheel.readthedocs.org/en/latest/#defining-conditional-dependencies # http://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies
':python_version == "2.6"' ':python_version == "2.6"'
' or python_version == "3.0"' ' or python_version == "3.0"'
' or python_version == "3.1" ': ['argparse>=1.2.1'], ' or python_version == "3.1" ': ['argparse>=1.2.1'],

14
tests/conftest.py Normal file
View File

@@ -0,0 +1,14 @@
import pytest
from pytest_httpbin.plugin import httpbin_ca_bundle
# Make httpbin's CA trusted by default
pytest.fixture(autouse=True)(httpbin_ca_bundle)
@pytest.fixture(scope='function')
def httpbin_secure_untrusted(monkeypatch, httpbin_secure):
"""Like the `httpbin_secure` fixture, but without the
make-CA-trusted-by-default"""
monkeypatch.delenv('REQUESTS_CA_BUNDLE')
return httpbin_secure

View File

@@ -12,7 +12,7 @@ def patharg(path):
return path.replace('\\', '\\\\\\') return path.replace('\\', '\\\\\\')
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__)), 'fixtures') FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__)))
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt') FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt')
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json') JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json')
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin') BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin')

View File

@@ -1,5 +1,5 @@
"""HTTP authentication-related tests.""" """HTTP authentication-related tests."""
import requests import mock
import pytest import pytest
from utils import http, add_auth, HTTP_OK, TestEnvironment from utils import http, add_auth, HTTP_OK, TestEnvironment
@@ -7,50 +7,53 @@ import httpie.input
import httpie.cli import httpie.cli
class TestAuth: def test_basic_auth(httpbin_both):
def test_basic_auth(self, httpbin):
r = http('--auth=user:password', r = http('--auth=user:password',
'GET', httpbin.url + '/basic-auth/user/password') 'GET', httpbin_both + '/basic-auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.skipif(
requests.__version__ == '0.13.6', @pytest.mark.parametrize('argument_name', ['--auth-type', '-A'])
reason='Redirects with prefetch=False are broken in Requests 0.13.6') def test_digest_auth(httpbin_both, argument_name):
def test_digest_auth(self, httpbin): r = http(argument_name + '=digest', '--auth=user:password',
r = http('--auth-type=digest', '--auth=user:password', 'GET', httpbin_both.url + '/digest-auth/auth/user/password')
'GET', httpbin.url + '/digest-auth/auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_password_prompt(self, httpbin):
httpie.input.AuthCredentials._getpass = lambda self, prompt: 'password' @mock.patch('httpie.input.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_password_prompt(httpbin):
r = http('--auth', 'user', r = http('--auth', 'user',
'GET', httpbin.url + '/basic-auth/user/password') 'GET', httpbin.url + '/basic-auth/user/password')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url(self, httpbin):
url = add_auth(httpbin.url + '/basic-auth/user/password', def test_credentials_in_url(httpbin_both):
url = add_auth(httpbin_both.url + '/basic-auth/user/password',
auth='user:password') auth='user:password')
r = http('GET', url) r = http('GET', url)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url_auth_flag_has_priority(self, httpbin):
def test_credentials_in_url_auth_flag_has_priority(httpbin_both):
"""When credentials are passed in URL and via -a at the same time, """When credentials are passed in URL and via -a at the same time,
then the ones from -a are used.""" then the ones from -a are used."""
url = add_auth(httpbin.url + '/basic-auth/user/password', url = add_auth(httpbin_both.url + '/basic-auth/user/password',
auth='user:wrong') auth='user:wrong')
r = http('--auth=user:password', 'GET', url) r = http('--auth=user:password', 'GET', url)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'} assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('url', [ @pytest.mark.parametrize('url', [
'username@example.org', 'username@example.org',
'username:@example.org', 'username:@example.org',
]) ])
def test_only_username_in_url(self, url): def test_only_username_in_url(url):
""" """
https://github.com/jkbrzt/httpie/issues/242 https://github.com/jkbrzt/httpie/issues/242

View File

@@ -6,6 +6,7 @@ from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
class TestBinaryRequestData: class TestBinaryRequestData:
def test_binary_stdin(self, httpbin): def test_binary_stdin(self, httpbin):
with open(BIN_FILE_PATH, 'rb') as stdin: with open(BIN_FILE_PATH, 'rb') as stdin:
env = TestEnvironment( env = TestEnvironment(

View File

@@ -68,10 +68,11 @@ class TestItemParsing:
def test_valid_items(self): def test_valid_items(self):
items = input.parse_items([ items = input.parse_items([
self.key_value('string=value'), 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('list:=["a", 1, {}, false]'),
self.key_value('obj:={"a": "b"}'), self.key_value('obj:={"a": "b"}'),
self.key_value('eh:'),
self.key_value('ed='), self.key_value('ed='),
self.key_value('bool:=true'), self.key_value('bool:=true'),
self.key_value('file@' + FILE_PATH_ARG), self.key_value('file@' + FILE_PATH_ARG),
@@ -83,7 +84,11 @@ class TestItemParsing:
# Parsed headers # Parsed headers
# `requests.structures.CaseInsensitiveDict` => `dict` # `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(items.headers._store.values()) headers = dict(items.headers._store.values())
assert headers == {'header': 'value', 'eh': ''} assert headers == {
'Header': 'value',
'Unset-Header': None,
'Empty-Header': ''
}
# Parsed data # Parsed data
raw_json_embed = items.data.pop('raw-json-embed') raw_json_embed = items.data.pop('raw-json-embed')
@@ -103,8 +108,8 @@ class TestItemParsing:
# Parsed file fields # Parsed file fields
assert 'file' in items.files assert 'file' in items.files
assert (items.files['file'][1].read().strip().decode('utf8') assert (items.files['file'][1].read().strip().
== FILE_CONTENT) decode('utf8') == FILE_CONTENT)
def test_multiple_file_fields_with_same_field_name(self): def test_multiple_file_fields_with_same_field_name(self):
items = input.parse_items([ items = input.parse_items([
@@ -154,7 +159,7 @@ class TestQuerystring:
assert '"url": "%s"' % url in r assert '"url": "%s"' % url in r
class TestURLshorthand: class TestLocalhostShorthand:
def test_expand_localhost_shorthand(self): def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=TestEnvironment()) args = parser.parse_args(args=[':'], env=TestEnvironment())
assert args.url == 'http://localhost' assert args.url == 'http://localhost'
@@ -201,7 +206,7 @@ class TestURLshorthand:
class TestArgumentParser: class TestArgumentParser:
def setup_method(self, method): def setup_method(self, method):
self.parser = input.Parser() self.parser = input.HTTPieArgumentParser()
def test_guess_when_method_set_and_valid(self): def test_guess_when_method_set_and_valid(self):
self.parser.args = argparse.Namespace() self.parser.args = argparse.Namespace()
@@ -325,8 +330,18 @@ class TestIgnoreStdin:
class TestSchemes: class TestSchemes:
def test_custom_scheme(self): def test_invalid_custom_scheme(self):
# InvalidSchema is expected because HTTPie # InvalidSchema is expected because HTTPie
# shouldn't touch a formally valid scheme. # shouldn't touch a formally valid scheme.
with pytest.raises(InvalidSchema): with pytest.raises(InvalidSchema):
http('foo+bar-BAZ.123://bah') 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')

33
tests/test_config.py Normal file
View File

@@ -0,0 +1,33 @@
from utils import TestEnvironment, http
def test_default_options(httpbin):
env = TestEnvironment()
env.config['default_options'] = ['--form']
env.config.save()
r = http(httpbin.url + '/post', 'foo=bar', env=env)
assert r.json['form'] == {"foo": "bar"}
def test_default_options_overwrite(httpbin):
env = TestEnvironment()
env.config['default_options'] = ['--form']
env.config.save()
r = http('--json', httpbin.url + '/post', 'foo=bar', env=env)
assert r.json['json'] == {"foo": "bar"}
def test_migrate_implicit_content_type():
config = TestEnvironment().config
config['implicit_content_type'] = 'json'
config.save()
config.load()
assert 'implicit_content_type' not in config
assert not config['default_options']
config['implicit_content_type'] = 'form'
config.save()
config.load()
assert 'implicit_content_type' not in config
assert config['default_options'] == ['--form']

View File

@@ -2,7 +2,8 @@
Tests for the provided defaults regarding HTTP method, and --json vs. --form. Tests for the provided defaults regarding HTTP method, and --json vs. --form.
""" """
from utils import TestEnvironment, http, HTTP_OK, no_content_type from httpie.client import JSON_ACCEPT
from utils import TestEnvironment, http, HTTP_OK
from fixtures import FILE_PATH from fixtures import FILE_PATH
@@ -46,7 +47,7 @@ class TestAutoContentTypeAndAcceptHeaders:
r = http('GET', httpbin.url + '/headers') r = http('GET', httpbin.url + '/headers')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Accept'] == '*/*' assert r.json['headers']['Accept'] == '*/*'
assert no_content_type(r.json['headers']) assert 'Content-Type' not in r.json['headers']
def test_POST_no_data_no_auto_headers(self, httpbin): def test_POST_no_data_no_auto_headers(self, httpbin):
# JSON headers shouldn't be automatically set for POST with no data. # JSON headers shouldn't be automatically set for POST with no data.
@@ -58,20 +59,20 @@ class TestAutoContentTypeAndAcceptHeaders:
def test_POST_with_data_auto_JSON_headers(self, httpbin): def test_POST_with_data_auto_JSON_headers(self, httpbin):
r = http('POST', httpbin.url + '/post', 'a=b') r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r assert HTTP_OK in r
assert '"Accept": "application/json"' in r assert r.json['headers']['Accept'] == JSON_ACCEPT
assert '"Content-Type": "application/json' in r assert r.json['headers']['Content-Type'] == 'application/json'
def test_GET_with_data_auto_JSON_headers(self, httpbin): def test_GET_with_data_auto_JSON_headers(self, httpbin):
# JSON headers should automatically be set also for GET with data. # JSON headers should automatically be set also for GET with data.
r = http('POST', httpbin.url + '/post', 'a=b') r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r assert HTTP_OK in r
assert '"Accept": "application/json"' in r, r assert r.json['headers']['Accept'] == JSON_ACCEPT
assert '"Content-Type": "application/json' in r assert r.json['headers']['Content-Type'] == 'application/json'
def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin): def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin):
r = http('--json', 'POST', httpbin.url + '/post') r = http('--json', 'POST', httpbin.url + '/post')
assert HTTP_OK in r 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. # Make sure Content-Type gets set even with no data.
# https://github.com/jkbrzt/httpie/issues/137 # https://github.com/jkbrzt/httpie/issues/137
assert 'application/json' in r.json['headers']['Content-Type'] assert 'application/json' in r.json['headers']['Content-Type']

View File

@@ -36,4 +36,4 @@ def test_rst_file_syntax(filename):
stdout=subprocess.PIPE stdout=subprocess.PIPE
) )
err = p.communicate()[1] err = p.communicate()[1]
assert p.returncode == 0, err assert p.returncode == 0, err.decode('utf8')

View File

@@ -2,12 +2,13 @@ import os
import time import time
import pytest import pytest
import mock
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from httpie.compat import urlopen from httpie.compat import urlopen
from httpie.downloads import ( from httpie.downloads import (
parse_content_range, filename_from_content_disposition, filename_from_url, parse_content_range, filename_from_content_disposition, filename_from_url,
get_unique_filename, ContentRangeError, Download, get_unique_filename, ContentRangeError, Downloader,
) )
from utils import http, TestEnvironment from utils import http, TestEnvironment
@@ -74,7 +75,31 @@ class TestDownloadUtils:
content_type='x-foo/bar' content_type='x-foo/bar'
) )
def test_unique_filename(self): @pytest.mark.parametrize(
'orig_name, unique_on_attempt, expected',
[
# Simple
('foo.bar', 0, 'foo.bar'),
('foo.bar', 1, 'foo.bar-1'),
('foo.bar', 10, 'foo.bar-10'),
# Trim
('A' * 20, 0, 'A' * 10),
('A' * 20, 1, 'A' * 8 + '-1'),
('A' * 20, 10, 'A' * 7 + '-10'),
# Trim before ext
('A' * 20 + '.txt', 0, 'A' * 6 + '.txt'),
('A' * 20 + '.txt', 1, 'A' * 4 + '.txt-1'),
# Trim at the end
('foo.' + 'A' * 20, 0, 'foo.' + 'A' * 6),
('foo.' + 'A' * 20, 1, 'foo.' + 'A' * 4 + '-1'),
('foo.' + 'A' * 20, 10, 'foo.' + 'A' * 3 + '-10'),
]
)
@mock.patch('httpie.downloads.get_filename_max_length')
def test_unique_filename(self, get_filename_max_length,
orig_name, unique_on_attempt,
expected):
def attempts(unique_on_attempt=0): def attempts(unique_on_attempt=0):
# noinspection PyUnresolvedReferences,PyUnusedLocal # noinspection PyUnresolvedReferences,PyUnusedLocal
def exists(filename): def exists(filename):
@@ -86,54 +111,55 @@ class TestDownloadUtils:
exists.attempt = 0 exists.attempt = 0
return exists return exists
assert 'foo.bar' == get_unique_filename('foo.bar', attempts(0)) get_filename_max_length.return_value = 10
assert 'foo.bar-1' == get_unique_filename('foo.bar', attempts(1))
assert 'foo.bar-10' == get_unique_filename('foo.bar', attempts(10)) actual = get_unique_filename(orig_name, attempts(unique_on_attempt))
assert expected == actual
class TestDownloads: class TestDownloads:
# TODO: more tests # TODO: more tests
def test_actual_download(self, httpbin): def test_actual_download(self, httpbin_both, httpbin):
url = httpbin.url + '/robots.txt' robots_txt = '/robots.txt'
body = urlopen(url).read().decode() body = urlopen(httpbin + robots_txt).read().decode()
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False) env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--download', url, env=env) r = http('--download', httpbin_both.url + robots_txt, env=env)
assert 'Downloading' in r.stderr assert 'Downloading' in r.stderr
assert '[K' in r.stderr assert '[K' in r.stderr
assert 'Done' in r.stderr assert 'Done' in r.stderr
assert body == r assert body == r
def test_download_with_Content_Length(self, httpbin): def test_download_with_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
download = Download(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
download.start(Response( downloader.start(Response(
url=httpbin.url + '/', url=httpbin_both.url + '/',
headers={'Content-Length': 10} headers={'Content-Length': 10}
)) ))
time.sleep(1.1) time.sleep(1.1)
download.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
time.sleep(1.1) time.sleep(1.1)
download.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
download.finish() downloader.finish()
assert not download.interrupted assert not downloader.interrupted
def test_download_no_Content_Length(self, httpbin): def test_download_no_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
download = Download(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
download.start(Response(url=httpbin.url + '/')) downloader.start(Response(url=httpbin_both.url + '/'))
time.sleep(1.1) time.sleep(1.1)
download.chunk_downloaded(b'12345') downloader.chunk_downloaded(b'12345')
download.finish() downloader.finish()
assert not download.interrupted assert not downloader.interrupted
def test_download_interrupted(self, httpbin): def test_download_interrupted(self, httpbin_both):
devnull = open(os.devnull, 'w') devnull = open(os.devnull, 'w')
download = Download(output_file=devnull, progress_file=devnull) downloader = Downloader(output_file=devnull, progress_file=devnull)
download.start(Response( downloader.start(Response(
url=httpbin.url + '/', url=httpbin_both.url + '/',
headers={'Content-Length': 5} headers={'Content-Length': 5}
)) ))
download.chunk_downloaded(b'1234') downloader.chunk_downloaded(b'1234')
download.finish() downloader.finish()
assert download.interrupted assert downloader.interrupted

View File

@@ -3,6 +3,7 @@ from pytest import raises
from requests import Request, Timeout from requests import Request, Timeout
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from httpie import ExitStatus
from httpie.core import main from httpie.core import main
error_msg = None error_msg = None
@@ -17,8 +18,8 @@ def test_error(get_response):
exc = ConnectionError('Connection aborted') exc = ConnectionError('Connection aborted')
exc.request = Request(method='GET', url='http://www.google.com') exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], error=error) ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == 1 assert ret == ExitStatus.ERROR
assert error_msg == ( assert error_msg == (
'ConnectionError: ' 'ConnectionError: '
'Connection aborted while doing GET request to URL: ' 'Connection aborted while doing GET request to URL: '
@@ -31,7 +32,7 @@ def test_error_traceback(get_response):
exc.request = Request(method='GET', url='http://www.google.com') exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc get_response.side_effect = exc
with raises(ConnectionError): with raises(ConnectionError):
ret = main(['--ignore-stdin', '--traceback', 'www.google.com']) main(['--ignore-stdin', '--traceback', 'www.google.com'])
@mock.patch('httpie.core.get_response') @mock.patch('httpie.core.get_response')
@@ -43,6 +44,6 @@ def test_timeout(get_response):
exc = Timeout('Request timed out') exc = Timeout('Request timed out')
exc.request = Request(method='GET', url='http://www.google.com') exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], error=error) ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == 2 assert ret == ExitStatus.ERROR_TIMEOUT
assert error_msg == 'Request timed out (30s).' assert error_msg == 'Request timed out (30s).'

View File

@@ -1,34 +1,29 @@
import requests
import pytest
from httpie import ExitStatus from httpie import ExitStatus
from utils import TestEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
class TestExitStatus: def test_ok_response_exits_0(httpbin):
def test_ok_response_exits_0(self, httpbin):
r = http('GET', httpbin.url + '/status/200') r = http('GET', httpbin.url + '/status/200')
assert HTTP_OK in r assert HTTP_OK in r
assert r.exit_status == ExitStatus.OK assert r.exit_status == ExitStatus.OK
def test_error_response_exits_0_without_check_status(self, httpbin):
def test_error_response_exits_0_without_check_status(httpbin):
r = http('GET', httpbin.url + '/status/500') r = http('GET', httpbin.url + '/status/500')
assert '500 INTERNAL SERVER ERRO' in r assert '500 INTERNAL SERVER ERRO' in r
assert r.exit_status == ExitStatus.OK assert r.exit_status == ExitStatus.OK
assert not r.stderr assert not r.stderr
@pytest.mark.skipif(
tuple(map(int, requests.__version__.split('.'))) < (2, 3, 0),
reason='timeout broken in requests prior v2.3.0 (#185)'
)
def test_timeout_exit_status(self, httpbin):
r = http('--timeout=0.5', 'GET', httpbin.url + '/delay/1', def test_timeout_exit_status(httpbin):
r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.02',
error_exit_ok=True) error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_TIMEOUT assert r.exit_status == ExitStatus.ERROR_TIMEOUT
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected( def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(
self, httpbin): httpbin):
env = TestEnvironment(stdout_isatty=False) env = TestEnvironment(stdout_isatty=False)
r = http('--check-status', '--headers', r = http('--check-status', '--headers',
'GET', httpbin.url + '/status/301', 'GET', httpbin.url + '/status/301',
@@ -37,10 +32,8 @@ class TestExitStatus:
assert r.exit_status == ExitStatus.ERROR_HTTP_3XX assert r.exit_status == ExitStatus.ERROR_HTTP_3XX
assert '301 moved permanently' in r.stderr.lower() assert '301 moved permanently' in r.stderr.lower()
@pytest.mark.skipif(
requests.__version__ == '0.13.6', def test_3xx_check_status_redirects_allowed_exits_0(httpbin):
reason='Redirects with prefetch=False are broken in Requests 0.13.6')
def test_3xx_check_status_redirects_allowed_exits_0(self, httpbin):
r = http('--check-status', '--follow', r = http('--check-status', '--follow',
'GET', httpbin.url + '/status/301', 'GET', httpbin.url + '/status/301',
error_exit_ok=True) error_exit_ok=True)
@@ -48,7 +41,8 @@ class TestExitStatus:
assert HTTP_OK in r assert HTTP_OK in r
assert r.exit_status == ExitStatus.OK assert r.exit_status == ExitStatus.OK
def test_4xx_check_status_exits_4(self, httpbin):
def test_4xx_check_status_exits_4(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/401', r = http('--check-status', 'GET', httpbin.url + '/status/401',
error_exit_ok=True) error_exit_ok=True)
assert '401 UNAUTHORIZED' in r assert '401 UNAUTHORIZED' in r
@@ -56,7 +50,8 @@ class TestExitStatus:
# Also stderr should be empty since stdout isn't redirected. # Also stderr should be empty since stdout isn't redirected.
assert not r.stderr assert not r.stderr
def test_5xx_check_status_exits_5(self, httpbin):
def test_5xx_check_status_exits_5(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/500', r = http('--check-status', 'GET', httpbin.url + '/status/500',
error_exit_ok=True) error_exit_ok=True)
assert '500 INTERNAL SERVER ERROR' in r assert '500 INTERNAL SERVER ERROR' in r

View File

@@ -1,5 +1,7 @@
"""High-level tests.""" """High-level tests."""
import pytest import pytest
from httpie.input import ParseError
from utils import TestEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
from fixtures import FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH, FILE_CONTENT
@@ -7,72 +9,101 @@ import httpie
from httpie.compat import is_py26 from httpie.compat import is_py26
class TestHTTPie: def test_debug():
def test_debug(self):
r = http('--debug') r = http('--debug')
assert r.exit_status == httpie.ExitStatus.OK assert r.exit_status == httpie.ExitStatus.OK
assert 'HTTPie %s' % httpie.__version__ in r.stderr assert 'HTTPie %s' % httpie.__version__ in r.stderr
assert 'HTTPie data:' in r.stderr
def test_help(self):
def test_help():
r = http('--help', error_exit_ok=True) r = http('--help', error_exit_ok=True)
assert r.exit_status == httpie.ExitStatus.OK assert r.exit_status == httpie.ExitStatus.OK
assert 'https://github.com/jkbrzt/httpie/issues' in r assert 'https://github.com/jkbrzt/httpie/issues' in r
def test_version(self):
def test_version():
r = http('--version', error_exit_ok=True) r = http('--version', error_exit_ok=True)
assert r.exit_status == httpie.ExitStatus.OK assert r.exit_status == httpie.ExitStatus.OK
# FIXME: py3 has version in stdout, py2 in stderr # FIXME: py3 has version in stdout, py2 in stderr
assert httpie.__version__ == r.stderr.strip() + r.strip() assert httpie.__version__ == r.stderr.strip() + r.strip()
def test_GET(self, httpbin):
r = http('GET', httpbin.url + '/get') def test_GET(httpbin_both):
r = http('GET', httpbin_both + '/get')
assert HTTP_OK in r assert HTTP_OK in r
def test_DELETE(self, httpbin):
r = http('DELETE', httpbin.url + '/delete') def test_DELETE(httpbin_both):
r = http('DELETE', httpbin_both + '/delete')
assert HTTP_OK in r assert HTTP_OK in r
def test_PUT(self, httpbin):
r = http('PUT', httpbin.url + '/put', 'foo=bar') def test_PUT(httpbin_both):
r = http('PUT', httpbin_both + '/put', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json']['foo'] == 'bar' assert r.json['json']['foo'] == 'bar'
def test_POST_JSON_data(self, httpbin):
r = http('POST', httpbin.url + '/post', 'foo=bar') def test_POST_JSON_data(httpbin_both):
r = http('POST', httpbin_both + '/post', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json']['foo'] == 'bar' assert r.json['json']['foo'] == 'bar'
def test_POST_form(self, httpbin):
r = http('--form', 'POST', httpbin.url + '/post', 'foo=bar') def test_POST_form(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar')
assert HTTP_OK in r assert HTTP_OK in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
def test_POST_form_multiple_values(self, httpbin):
r = http('--form', 'POST', httpbin.url + '/post', 'foo=bar', 'foo=baz') def test_POST_form_multiple_values(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar', 'foo=baz')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['form'] == {'foo': ['bar', 'baz']} assert r.json['form'] == {'foo': ['bar', 'baz']}
def test_POST_stdin(self, httpbin):
def test_POST_stdin(httpbin_both):
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
env = TestEnvironment(stdin=f, stdin_isatty=False) env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--form', 'POST', httpbin.url + '/post', env=env) r = http('--form', 'POST', httpbin_both + '/post', env=env)
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
def test_headers(self, httpbin):
r = http('GET', httpbin.url + '/headers', 'Foo:bar') def test_headers(httpbin_both):
r = http('GET', httpbin_both + '/headers', 'Foo:bar')
assert HTTP_OK in r assert HTTP_OK in r
assert '"User-Agent": "HTTPie' in r, r assert '"User-Agent": "HTTPie' in r, r
assert '"Foo": "bar"' in r assert '"Foo": "bar"' in r
def test_headers_unset(httpbin_both):
r = http('GET', httpbin_both + '/headers')
assert 'Accept' in r.json['headers'] # default Accept present
r = http('GET', httpbin_both + '/headers', 'Accept:')
assert 'Accept' not in r.json['headers'] # default Accept unset
def test_headers_empty_value(httpbin_both):
r = http('GET', httpbin_both + '/headers')
assert r.json['headers']['Accept'] # default Accept has value
r = http('GET', httpbin_both + '/headers', 'Accept;')
assert r.json['headers']['Accept'] == '' # Accept has no value
def test_headers_empty_value_with_value_gives_error(httpbin):
with pytest.raises(ParseError):
http('GET', httpbin + '/headers', 'Accept;SYNTAX_ERROR')
@pytest.mark.skipif( @pytest.mark.skipif(
is_py26, is_py26,
reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only' reason='the `object_pairs_hook` arg for `json.loads()` is Py>2.6 only'
) )
def test_json_input_preserve_order(self, httpbin): def test_json_input_preserve_order(httpbin_both):
r = http('PATCH', httpbin.url + '/patch', r = http('PATCH', httpbin_both + '/patch',
'order:={"map":{"1":"first","2":"second"}}') 'order:={"map":{"1":"first","2":"second"}}')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['data'] == \ assert r.json['data'] == \

View File

@@ -1,10 +1,30 @@
import os
from tempfile import gettempdir
import pytest import pytest
from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF
from httpie import ExitStatus from httpie import ExitStatus
from httpie.compat import urlopen
from httpie.output.formatters.colors import get_lexer from httpie.output.formatters.colors import get_lexer
@pytest.mark.parametrize('stdout_isatty', [True, False])
def test_output_option(httpbin, stdout_isatty):
output_filename = os.path.join(gettempdir(), test_output_option.__name__)
url = httpbin + '/robots.txt'
r = http('--output', output_filename, url,
env=TestEnvironment(stdout_isatty=stdout_isatty))
assert r == ''
expected_body = urlopen(url).read().decode()
with open(output_filename, 'r') as f:
actual_body = f.read()
assert actual_body == expected_body
class TestVerboseFlag: class TestVerboseFlag:
def test_verbose(self, httpbin): def test_verbose(self, httpbin):
r = http('--verbose', r = http('--verbose',
@@ -25,25 +45,38 @@ class TestVerboseFlag:
assert HTTP_OK in r assert HTTP_OK in r
assert '"baz": "bar"' in r assert '"baz": "bar"' in r
def test_verbose_implies_all(self, httpbin):
r = http('--verbose', '--follow', httpbin + '/redirect/1')
assert 'GET /redirect/1 HTTP/1.1' in r
assert 'HTTP/1.1 302 FOUND' in r
assert 'GET /get HTTP/1.1' in r
assert HTTP_OK in r
class TestColors: class TestColors:
@pytest.mark.parametrize('mime', [ @pytest.mark.parametrize(
'application/json', argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
'application/json+foo', argvalues=[
'application/foo+json', ('application/json', False, None, 'JSON'),
'application/json-foo', ('application/json+foo', False, None, 'JSON'),
'application/x-json', ('application/foo+json', False, None, 'JSON'),
'foo/json', ('application/json-foo', False, None, 'JSON'),
'foo/json+bar', ('application/x-json', False, None, 'JSON'),
'foo/bar+json', ('foo/json', False, None, 'JSON'),
'foo/json-foo', ('foo/json+bar', False, None, 'JSON'),
'foo/x-json', ('foo/bar+json', False, None, 'JSON'),
]) ('foo/json-foo', False, None, 'JSON'),
def test_get_lexer(self, mime): ('foo/x-json', False, None, 'JSON'),
lexer = get_lexer(mime) ('application/vnd.comverge.grid+hal+json', False, None, 'JSON'),
('text/plain', True, '{}', 'JSON'),
('text/plain', True, 'foo', 'Text only'),
]
)
def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
lexer = get_lexer(mime, body=body, explicit_json=explicit_json)
assert lexer is not None assert lexer is not None
assert lexer.name == 'JSON' assert lexer.name == expected_lexer_name
def test_get_lexer_not_found(self): def test_get_lexer_not_found(self):
assert get_lexer('xxx/yyy') is None assert get_lexer('xxx/yyy') is None

47
tests/test_redirects.py Normal file
View File

@@ -0,0 +1,47 @@
"""High-level tests."""
import pytest
from httpie import ExitStatus
from utils import http, HTTP_OK
def test_follow_all_redirects_shown(httpbin):
r = http('--follow', '--all', httpbin.url + '/redirect/2')
assert r.count('HTTP/1.1') == 3
assert r.count('HTTP/1.1 302 FOUND', 2)
assert HTTP_OK in r
@pytest.mark.parametrize('follow_flag', ['--follow', '-F'])
def test_follow_without_all_redirects_hidden(httpbin, follow_flag):
r = http(follow_flag, httpbin.url + '/redirect/2')
assert r.count('HTTP/1.1') == 1
assert HTTP_OK in r
def test_follow_all_output_options_used_for_redirects(httpbin):
r = http('--check-status',
'--follow',
'--all',
'--print=H',
httpbin.url + '/redirect/2')
assert r.count('GET /') == 3
assert HTTP_OK not in r
def test_follow_redirect_output_options(httpbin):
r = http('--check-status',
'--follow',
'--all',
'--print=h',
'--history-print=H',
httpbin.url + '/redirect/2')
assert r.count('GET /') == 2
assert 'HTTP/1.1 302 FOUND' not in r
assert HTTP_OK in r
def test_max_redirects(httpbin):
r = http('--max-redirects=1', '--follow', httpbin.url + '/redirect/3',
error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_TOO_MANY_REDIRECTS

View File

@@ -2,12 +2,12 @@
import os import os
import shutil import shutil
import sys import sys
from tempfile import gettempdir
import pytest import pytest
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from utils import TestEnvironment, mk_config_dir, http, HTTP_OK, \ from utils import TestEnvironment, mk_config_dir, http, HTTP_OK
no_content_type
from fixtures import UNICODE from fixtures import UNICODE
@@ -125,7 +125,7 @@ class TestSession(SessionTestBase):
r2 = http('--session=test', 'GET', httpbin.url + '/get', r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env()) env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
assert no_content_type(r2.json['headers']) assert 'Content-Type' not in r2.json['headers']
assert 'If-Unmodified-Since' not in r2.json['headers'] assert 'If-Unmodified-Since' not in r2.json['headers']
def test_session_by_path(self, httpbin): def test_session_by_path(self, httpbin):
@@ -157,8 +157,8 @@ class TestSession(SessionTestBase):
assert HTTP_OK in r2 assert HTTP_OK in r2
# FIXME: Authorization *sometimes* is not present on Python3 # FIXME: Authorization *sometimes* is not present on Python3
assert (r2.json['headers']['Authorization'] assert (r2.json['headers']['Authorization'] ==
== HTTPBasicAuth.make_header(u'test', UNICODE)) HTTPBasicAuth.make_header(u'test', UNICODE))
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
assert UNICODE in r2 assert UNICODE in r2
@@ -174,3 +174,14 @@ class TestSession(SessionTestBase):
r2 = http('--session=test', httpbin.url + '/headers', env=self.env()) r2 = http('--session=test', httpbin.url + '/headers', env=self.env())
assert HTTP_OK in r2 assert HTTP_OK in r2
assert r2.json['headers']['User-Agent'] == 'custom' assert r2.json['headers']['User-Agent'] == 'custom'
def test_download_in_session(self, httpbin):
# https://github.com/jkbrzt/httpie/issues/412
self.start_session(httpbin)
cwd = os.getcwd()
os.chdir(gettempdir())
try:
http('--session=test', '--download',
httpbin.url + '/get', env=self.env())
finally:
os.chdir(cwd)

View File

@@ -5,6 +5,7 @@ import pytest_httpbin.certs
from requests.exceptions import SSLError from requests.exceptions import SSLError
from httpie import ExitStatus from httpie import ExitStatus
from httpie.input import SSL_VERSION_ARG_MAPPING
from utils import http, HTTP_OK, TESTS_ROOT from utils import http, HTTP_OK, TESTS_ROOT
@@ -12,17 +13,44 @@ CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key') CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem') 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. # We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error. # Requests without --verify=<CA_BUNDLE> will fail with a verification error.
# See: https://github.com/kevin1024/pytest-httpbin#https-support # See: https://github.com/kevin1024/pytest-httpbin#https-support
CA_BUNDLE = pytest_httpbin.certs.where() CA_BUNDLE = pytest_httpbin.certs.where()
class TestClientSSLCertHandling(object): @pytest.mark.parametrize('ssl_version', SSL_VERSION_ARG_MAPPING.keys())
def test_ssl_version(httpbin_secure, ssl_version):
try:
r = http(
'--ssl', ssl_version,
httpbin_secure + '/get'
)
assert HTTP_OK in r
except SSLError as e:
if ssl_version == 'ssl3':
# pytest-httpbin doesn't support ssl3
assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e)
else:
raise
class TestClientCert:
def test_cert_and_key(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--cert', CLIENT_CERT,
'--cert-key', CLIENT_KEY)
assert HTTP_OK in r
def test_cert_pem(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--cert', CLIENT_PEM)
assert HTTP_OK in r
def test_cert_file_not_found(self, httpbin_secure): def test_cert_file_not_found(self, httpbin_secure):
r = http(httpbin_secure + '/get', r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', '/__not_found__', '--cert', '/__not_found__',
error_exit_ok=True) error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR assert r.exit_status == ExitStatus.ERROR
@@ -31,45 +59,31 @@ class TestClientSSLCertHandling(object):
def test_cert_file_invalid(self, httpbin_secure): def test_cert_file_invalid(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(SSLError):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', __file__) '--cert', __file__)
def test_cert_ok_but_missing_key(self, httpbin_secure): def test_cert_ok_but_missing_key(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(SSLError):
http(httpbin_secure + '/get', http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_CERT) '--cert', CLIENT_CERT)
def test_cert_and_key(self, httpbin_secure):
r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_CERT,
'--cert-key', CLIENT_KEY)
assert HTTP_OK in r
def test_cert_pem(self, httpbin_secure): class TestServerCert:
r = http(httpbin_secure + '/get',
'--verify', CA_BUNDLE,
'--cert', CLIENT_PEM)
assert HTTP_OK in r
class TestServerSSLCertHandling(object):
def test_self_signed_server_cert_by_default_raises_ssl_error(
self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure.url + '/get')
def test_verify_no_OK(self, httpbin_secure): def test_verify_no_OK(self, httpbin_secure):
r = http(httpbin_secure.url + '/get', '--verify=no') r = http(httpbin_secure.url + '/get', '--verify=no')
assert HTTP_OK in r assert HTTP_OK in r
def test_verify_custom_ca_bundle_path( def test_verify_custom_ca_bundle_path(
self, httpbin_secure): self, httpbin_secure_untrusted):
r = http(httpbin_secure.url + '/get', '--verify', CA_BUNDLE) r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE)
assert HTTP_OK in r assert HTTP_OK in r
def test_self_signed_server_cert_by_default_raises_ssl_error(
self,
httpbin_secure_untrusted):
with pytest.raises(SSLError):
http(httpbin_secure_untrusted.url + '/get')
def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure):
with pytest.raises(SSLError): with pytest.raises(SSLError):
http(httpbin_secure.url + '/get', '--verify', '/__not_found__') http(httpbin_secure.url + '/get', '--verify', '/__not_found__')

View File

@@ -6,12 +6,12 @@ from utils import http, TestEnvironment
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
class TestStream:
# GET because httpbin 500s with binary POST body. # GET because httpbin 500s with binary POST body.
@pytest.mark.skipif(is_windows, @pytest.mark.skipif(is_windows,
reason='Pretty redirect not supported under Windows') reason='Pretty redirect not supported under Windows')
def test_pretty_redirected_stream(self, httpbin): def test_pretty_redirected_stream(httpbin):
"""Test that --stream works with prettified redirected output.""" """Test that --stream works with prettified redirected output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
env = TestEnvironment(colors=256, stdin=f, env = TestEnvironment(colors=256, stdin=f,
@@ -21,7 +21,8 @@ class TestStream:
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_encoded_stream(self, httpbin):
def test_encoded_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
@@ -30,7 +31,8 @@ class TestStream:
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_redirected_stream(self, httpbin):
def test_redirected_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:

View File

@@ -7,64 +7,72 @@ from utils import http, HTTP_OK
from fixtures import UNICODE from fixtures import UNICODE
class TestUnicode: def test_unicode_headers(httpbin):
def test_unicode_headers(self, httpbin):
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE) r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
def test_unicode_headers_verbose(self, httpbin):
def test_unicode_headers_verbose(httpbin):
# httpbin doesn't interpret utf8 headers # httpbin doesn't interpret utf8 headers
r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE) r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_form_item(self, httpbin):
def test_unicode_form_item(httpbin):
r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['form'] == {'test': UNICODE} assert r.json['form'] == {'test': UNICODE}
def test_unicode_form_item_verbose(self, httpbin):
def test_unicode_form_item_verbose(httpbin):
r = http('--verbose', '--form', r = http('--verbose', '--form',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE) 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_json_item(self, httpbin):
def test_unicode_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE) r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': UNICODE} assert r.json['json'] == {'test': UNICODE}
def test_unicode_json_item_verbose(self, httpbin):
def test_unicode_json_item_verbose(httpbin):
r = http('--verbose', '--json', r = http('--verbose', '--json',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE) 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_raw_json_item(self, httpbin):
def test_unicode_raw_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_raw_json_item_verbose(self, httpbin):
def test_unicode_raw_json_item_verbose(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE)) u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}} assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_url_query_arg_item(self, httpbin):
def test_unicode_url_query_arg_item(httpbin):
r = http(httpbin.url + '/get', u'test==%s' % UNICODE) r = http(httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE}, r assert r.json['args'] == {'test': UNICODE}, r
def test_unicode_url_query_arg_item_verbose(self, httpbin):
def test_unicode_url_query_arg_item_verbose(httpbin):
r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE) r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert UNICODE in r assert UNICODE in r
def test_unicode_url(self, httpbin):
def test_unicode_url(httpbin):
r = http(httpbin.url + u'/get?test=' + UNICODE) r = http(httpbin.url + u'/get?test=' + UNICODE)
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE} assert r.json['args'] == {'test': UNICODE}
@@ -73,13 +81,15 @@ class TestUnicode:
# r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE) # r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE)
# assert HTTP_OK in r # assert HTTP_OK in r
def test_unicode_basic_auth(self, httpbin):
def test_unicode_basic_auth(httpbin):
# it doesn't really authenticate us because httpbin # it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth # doesn't interpret the utf8-encoded auth
http('--verbose', '--auth', u'test:%s' % UNICODE, http('--verbose', '--auth', u'test:%s' % UNICODE,
httpbin.url + u'/basic-auth/test/' + UNICODE) httpbin.url + u'/basic-auth/test/' + UNICODE)
def test_unicode_digest_auth(self, httpbin):
def test_unicode_digest_auth(httpbin):
# it doesn't really authenticate us because httpbin # it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth # doesn't interpret the utf8-encoded auth
http('--auth-type=digest', http('--auth-type=digest',

View File

@@ -23,6 +23,7 @@ class TestMultipartFormDataFileUpload:
' filename="%s"' % os.path.basename(FILE_PATH) in r ' filename="%s"' % os.path.basename(FILE_PATH) in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
assert 'Content-Type: text/plain' in r
def test_upload_multiple_fields_with_the_same_name(self, httpbin): def test_upload_multiple_fields_with_the_same_name(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
@@ -34,6 +35,7 @@ class TestMultipartFormDataFileUpload:
# Should be 4, but is 3 because httpbin # Should be 4, but is 3 because httpbin
# doesn't seem to support filed field lists # doesn't seem to support filed field lists
assert r.count(FILE_CONTENT) in [3, 4] assert r.count(FILE_CONTENT) in [3, 4]
assert r.count('Content-Type: text/plain') == 2
class TestRequestBodyFromFilePath: class TestRequestBodyFromFilePath:

View File

@@ -22,7 +22,9 @@ class TestFakeWindows:
def test_output_file_pretty_not_allowed_on_windows(self, httpbin): def test_output_file_pretty_not_allowed_on_windows(self, httpbin):
env = TestEnvironment(is_windows=True) env = TestEnvironment(is_windows=True)
output_file = os.path.join( output_file = os.path.join(
tempfile.gettempdir(), '__httpie_test_output__') tempfile.gettempdir(),
self.test_output_file_pretty_not_allowed_on_windows.__name__
)
r = http('--output', output_file, r = http('--output', output_file,
'--pretty=all', 'GET', httpbin.url + '/get', '--pretty=all', 'GET', httpbin.url + '/get',
env=env, error_exit_ok=True) env=env, error_exit_ok=True)

View File

@@ -1,23 +1,18 @@
# coding=utf-8 # coding=utf-8
"""Utilities used by HTTPie tests. """Utilities for HTTPie test suite."""
"""
import os import os
import sys import sys
import time import time
import json import json
import shutil
import tempfile import tempfile
import httpie from httpie import ExitStatus, EXIT_STATUS_LABELS
from httpie.context import Environment from httpie.context import Environment
from httpie.core import main from httpie.core import main
from httpie.compat import bytes, str from httpie.compat import bytes, str
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
CRLF = '\r\n' CRLF = '\r\n'
COLOR = '\x1b[' COLOR = '\x1b['
HTTP_OK = '200 OK' HTTP_OK = '200 OK'
@@ -28,14 +23,9 @@ HTTP_OK_COLOR = (
) )
def no_content_type(headers): def mk_config_dir():
return ( dirname = tempfile.mkdtemp(prefix='httpie_config_')
'Content-Type' not in headers return dirname
# We need to do also this because of this issue:
# <https://github.com/kevin1024/pytest-httpbin/issues/5>
# TODO: remove this function once the issue is if fixed
or headers['Content-Type'] == 'text/plain'
)
def add_auth(url, auth): def add_auth(url, auth):
@@ -44,136 +34,44 @@ def add_auth(url, auth):
class TestEnvironment(Environment): class TestEnvironment(Environment):
""" """Environment subclass with reasonable defaults for testing."""
Environment subclass with reasonable defaults suitable for testing.
"""
colors = 0 colors = 0
stdin_isatty = True, stdin_isatty = True,
stdout_isatty = True stdout_isatty = True
is_windows = False is_windows = False
_shutil = shutil # needed by __del__ (would get gc'd)
def __init__(self, **kwargs): def __init__(self, **kwargs):
if 'stdout' not in kwargs: if 'stdout' not in kwargs:
kwargs['stdout'] = tempfile.TemporaryFile('w+b') kwargs['stdout'] = tempfile.TemporaryFile(
mode='w+b',
prefix='httpie_stdout'
)
if 'stderr' not in kwargs: if 'stderr' not in kwargs:
kwargs['stderr'] = tempfile.TemporaryFile('w+t') kwargs['stderr'] = tempfile.TemporaryFile(
mode='w+t',
self.delete_config_dir = False prefix='httpie_stderr'
if 'config_dir' not in kwargs: )
kwargs['config_dir'] = mk_config_dir()
self.delete_config_dir = True
super(TestEnvironment, self).__init__(**kwargs) super(TestEnvironment, self).__init__(**kwargs)
self._delete_config_dir = False
@property
def config(self):
if not self.config_dir.startswith(tempfile.gettempdir()):
self.config_dir = mk_config_dir()
self._delete_config_dir = True
return super(TestEnvironment, self).config
def cleanup(self):
if self._delete_config_dir:
assert self.config_dir.startswith(tempfile.gettempdir())
from shutil import rmtree
rmtree(self.config_dir)
def __del__(self): def __del__(self):
if self.delete_config_dir:
self._shutil.rmtree(self.config_dir)
def http(*args, **kwargs):
"""
Run HTTPie and capture stderr/out and exit status.
Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output.
The response has the following attributes:
`stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr
`exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated.
If you pass ``error_exit_ok=True``, then error exit statuses
won't result into an exception.
Example:
$ http --auth=user:password GET httpbin.org/basic-auth/user/password
>>> r = http('-a', 'user:pw', 'httpbin.org/basic-auth/user/pw')
>>> type(r) == StrCLIResponse
True
>>> r.exit_status
0
>>> r.stderr
''
>>> 'HTTP/1.1 200 OK' in r
True
>>> r.json == {'authenticated': True, 'user': 'user'}
True
"""
error_exit_ok = kwargs.pop('error_exit_ok', False)
env = kwargs.get('env')
if not env:
env = kwargs['env'] = TestEnvironment()
stdout = env.stdout
stderr = env.stderr
args = list(args)
if '--debug' not in args and '--traceback' not in args:
args = ['--traceback'] + args
def dump_stderr():
stderr.seek(0)
sys.stderr.write(stderr.read())
try: try:
try: self.cleanup()
exit_status = main(args=args, **kwargs)
if '--download' in args:
# Let the progress reporter thread finish.
time.sleep(.5)
except SystemExit:
if error_exit_ok:
exit_status = httpie.ExitStatus.ERROR
else:
dump_stderr()
raise
except Exception: except Exception:
stderr.seek(0) pass
sys.stderr.write(stderr.read())
raise
else:
if exit_status != httpie.ExitStatus.OK and not error_exit_ok:
dump_stderr()
raise Exception('Unexpected exit status: %s', exit_status)
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try:
output = output.decode('utf8')
except UnicodeDecodeError:
# noinspection PyArgumentList
r = BytesCLIResponse(output)
else:
# noinspection PyArgumentList
r = StrCLIResponse(output)
r.stderr = stderr.read()
r.exit_status = exit_status
if r.exit_status != httpie.ExitStatus.OK:
sys.stderr.write(r.stderr)
return r
finally:
stdout.close()
stderr.close()
class BaseCLIResponse(object): class BaseCLIResponse(object):
@@ -221,8 +119,8 @@ class StrCLIResponse(str, BaseCLIResponse):
elif self.strip().startswith('{'): elif self.strip().startswith('{'):
# Looks like JSON body. # Looks like JSON body.
self._json = json.loads(self) self._json = json.loads(self)
elif (self.count('Content-Type:') == 1 elif (self.count('Content-Type:') == 1 and
and 'application/json' in self): 'application/json' in self):
# Looks like a whole JSON HTTP message, # Looks like a whole JSON HTTP message,
# try to extract its body. # try to extract its body.
try: try:
@@ -237,5 +135,120 @@ class StrCLIResponse(str, BaseCLIResponse):
return self._json return self._json
def mk_config_dir(): class ExitStatusError(Exception):
return tempfile.mkdtemp(prefix='httpie_test_config_dir_') pass
def http(*args, **kwargs):
# noinspection PyUnresolvedReferences
"""
Run HTTPie and capture stderr/out and exit status.
Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output.
The response has the following attributes:
`stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr
`exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated.
If you pass ``error_exit_ok=True``, then error exit statuses
won't result into an exception.
Example:
$ http --auth=user:password GET httpbin.org/basic-auth/user/password
>>> httpbin = getfixture('httpbin')
>>> r = http('-a', 'user:pw', httpbin.url + '/basic-auth/user/pw')
>>> type(r) == StrCLIResponse
True
>>> r.exit_status
0
>>> r.stderr
''
>>> 'HTTP/1.1 200 OK' in r
True
>>> r.json == {'authenticated': True, 'user': 'user'}
True
"""
error_exit_ok = kwargs.pop('error_exit_ok', False)
env = kwargs.get('env')
if not env:
env = kwargs['env'] = TestEnvironment()
stdout = env.stdout
stderr = env.stderr
args = list(args)
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:
add_to_args.append('--traceback')
if not any('--timeout' in arg for arg in args_with_config_defaults):
add_to_args.append('--timeout=3')
args = add_to_args + args
def dump_stderr():
stderr.seek(0)
sys.stderr.write(stderr.read())
try:
try:
exit_status = main(args=args, **kwargs)
if '--download' in args:
# Let the progress reporter thread finish.
time.sleep(.5)
except SystemExit:
if error_exit_ok:
exit_status = ExitStatus.ERROR
else:
dump_stderr()
raise
except Exception:
stderr.seek(0)
sys.stderr.write(stderr.read())
raise
else:
if not error_exit_ok and exit_status != ExitStatus.OK:
dump_stderr()
raise ExitStatusError(
'httpie.core.main() unexpectedly returned'
' a non-zero exit status: {0} ({1})'.format(
exit_status,
EXIT_STATUS_LABELS[exit_status]
)
)
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try:
output = output.decode('utf8')
except UnicodeDecodeError:
# noinspection PyArgumentList
r = BytesCLIResponse(output)
else:
# noinspection PyArgumentList
r = StrCLIResponse(output)
r.stderr = stderr.read()
r.exit_status = exit_status
if r.exit_status != ExitStatus.OK:
sys.stderr.write(r.stderr)
return r
finally:
stdout.close()
stderr.close()
env.cleanup()

24
tox.ini
View File

@@ -1,18 +1,30 @@
# Tox (http://tox.testrun.org/) is a tool for running tests # Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. # in multiple virtualenvs. See ./CONTRIBUTING.rst
# Run:
# $ pip install -r requirements-dev.txt
# $ tox
[tox] [tox]
envlist = py26, py27, py34, pypy envlist = py26, py27, py35, pypy, codestyle
[testenv] [testenv]
deps = deps =
mock
pytest pytest
pytest-httpbin>=0.0.6 pytest-httpbin>=0.0.6
commands = commands =
py.test --verbose --doctest-modules --basetemp={envtmpdir} {posargs:./tests ./httpie} # NOTE: the order of the directories in posargs seems to matter.
# When changed, then many ImportMismatchError exceptions occurrs.
py.test \
--verbose \
--doctest-modules \
{posargs:./httpie ./tests}
[testenv:codestyle]
deps = pycodestyle
commands =
pycodestyle \
--ignore=E241,E501
# 241 - multiple spaces after ,
# 501 - line too long