Compare commits

...

771 Commits
0.2.1 ... 0.9.8

Author SHA1 Message Date
3a3aecca45 0.9.8 2016-12-08 05:22:20 +01:00
fb3a26586a Fix --auth-type help 2016-12-08 05:16:22 +01:00
cc9083f541 Keep the latest submitted Homebrew formula in extras/ for testing 2016-12-08 04:58:49 +01:00
9ae86f3b4f 0.9.7 2016-12-08 04:47:32 +01:00
3a6fd074a1 Added ExitStatus.PLUGIN_ERROR (7) 2016-12-08 04:42:17 +01:00
da59381b0b Fix PyPi link 2016-12-07 18:54:53 +01:00
6de2d6c2cb Docs 2016-12-07 06:20:01 +01:00
b9b033ed0c Docs 2016-12-07 06:00:51 +01:00
64d6363565 Docs 2016-12-07 05:59:27 +01:00
923b7acbe6 Docs 2016-12-07 05:56:53 +01:00
2efc0db8d4 Cleanup 2016-11-24 00:58:41 +01:00
2bf71af286 pep8 2016-11-23 23:36:46 +01:00
0b84180485 Fix Python 2.6 2016-11-23 23:20:52 +01:00
5a1bd4ba83 Cleanup 2016-11-23 23:15:18 +01:00
3f7ed35238 Add more plugin API tests 2016-11-23 23:09:45 +01:00
47fd392c74 Cleanup 2016-11-23 22:33:22 +01:00
54a63a810e Cleanup/docstring 2016-11-23 22:29:36 +01:00
a49774d3ab Extend auth plugin API
This extends the `AuthPlugin` API by the following attributes:

* `auth_require`: set to `False` to make `--auth, -a` optional
* `auth_parse`: set to `False` to disable `username:password` parsing
  (access the raw value passed to `-a` via `self.raw_auth`).
* `prompt_password`: set to`False` to disable password prompt when
   no password provided (only relevant when `auth_parse == True`)

 These changes should be 100% backwards-compatible.

 What needs more testing is auth support in sessions.

Close #433
Close #431
Close #378
Ping teracyhq/httpie-jwt-auth#3
2016-11-23 22:02:12 +01:00
b879d38b07 Test case for Host header removal (unimplemented feature) 2016-11-23 22:02:12 +01:00
0913e8b2ef Merge pull request #533 from kigawas/patch-1
Update README.rst
2016-10-28 18:11:55 +02:00
4fef4b9a75 Update README.rst
Change "you shell" to "your shell"
2016-10-28 12:02:21 +08:00
bfc23b1412 Changelog 2016-10-26 12:18:53 +02:00
6267f21f21 Clean-up 2016-10-26 11:58:47 +02:00
e9aba543b1 Changelog 2016-10-26 11:54:35 +02:00
9b23a4ac9a Exit with status 130 on CTRL-C
http://www.tldp.org/LDP/abs/html/exitcodes.html

 #531
2016-10-26 11:53:01 +02:00
b96eba336d Fixed test 2016-10-26 11:28:17 +02:00
48a6d234cb Need a main()
#531
2016-10-26 11:21:30 +02:00
c6f2b32e36 Stricter KeyboardInterrupt silencing
Relates to #531, but doesn't solve it completely.
2016-10-26 11:16:39 +02:00
64f6f69037 Add Twitter link 2016-09-17 15:58:05 +02:00
6bdfc7a071 Update config and session file help URLs 2016-09-12 10:57:30 +02:00
497a91711a README 2016-09-12 09:13:37 +02:00
f515ef72d0 README 2016-09-12 09:12:07 +02:00
22a2fddc79 README 2016-09-12 08:59:55 +02:00
1847eaa299 Updated config docs 2016-09-11 18:48:56 +02:00
e387c1d43e Updated config docs 2016-09-11 18:46:33 +02:00
fc6d89913f README 2016-09-11 11:39:03 +02:00
d584686744 README 2016-09-11 01:16:07 +02:00
b565be4318 CHANGELOG 2016-09-06 11:53:52 +01:00
87e44ae639 Handle curses-free Pythons 2016-09-06 11:50:56 +01:00
0d08732397 Merge pull request #516 from dongweiming/fix-496
Fix the handling of zero REQUEST_ITEM arguments 

Close  #496
2016-09-06 11:06:45 +01:00
c53a778f60 Fix Issue #496 2016-09-01 17:46:34 +08:00
5efc9010cc Update CHANGELOG.rst 2016-08-14 11:36:21 +02:00
08e883fcfe Merge pull request #503 from zquestz/patch-1
Updated README.rst to add Arch Linux install docs.
2016-08-14 04:09:50 +02:00
c4b309164f Updated README.rst to add Arch Linux install docs. 2016-08-13 19:08:37 -07:00
8e96238323 v0.9.6 2016-08-13 23:01:05 +02:00
8a9206eceb Fixed Makefile 2016-08-13 22:57:44 +02:00
8ac3c5961c Upgrade Pygments version 2016-08-13 22:57:33 +02:00
487c7a9221 v0.9.5 2016-08-13 22:51:42 +02:00
6d65668355 Strip request header values 2016-08-13 22:40:01 +02:00
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
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
af737fd338 Fixed spelling mistage GitHib to GitHub 2016-08-11 01:43:15 +02:00
ee375b6942 Merge pull request #493 from medecau/codestyle_environment
Codestyle environment
2016-07-29 23:17:00 +02:00
becb63de9a useful info 2016-07-26 21:59:34 +01:00
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
8f6bee9196 codestyle fixes 2016-07-19 17:23:40 +01:00
9c2c058ae5 separate environment to test codestyle as proposed by @sigmavirus24 2016-07-19 17:23:18 +01:00
6238b59e72 Fix formatting 2016-07-08 15:05:43 +02:00
702c21aa91 Added related projects 2016-07-08 15:03:48 +02:00
aab5cd9da0 PEP8. clean-up 2016-07-04 20:30:55 +02:00
8c0f0b578c Clean-up 2016-07-02 18:44:02 +02:00
bb4881a873 Fixed README 2016-07-02 18:30:04 +02:00
3a1726b4ed Fixed README 2016-07-02 15:04:19 +02:00
e1fa57d228 Added -I as a shortcut for --ignore-stdin 2016-07-02 15:01:46 +02:00
bfc64bce21 Upgrade requests to 2.10.0 to enable optional SOCKS support
Closes #86
2016-07-02 14:58:34 +02:00
595dc51b2d Fish shell completion 2016-07-02 14:33:04 +02:00
83fa772247 Merge pull request #459 from dickeyxxx/fish-completion
added completions for fish shell
2016-07-02 14:31:06 +02:00
49a0fb6e0f More liberal default JSON Accept header
Closes #470
2016-07-02 14:18:36 +02:00
41e822ca2f Clean-up 2016-07-02 12:51:35 +02:00
1124d68946 Added --default-scheme <URL_SCHEME>
Closes #289
2016-07-02 12:47:02 +02:00
c3735d0422 Merge pull request #401 from lgarron/default-scheme
Add a --default-scheme argument.
2016-07-02 12:32:07 +02:00
364b91cbc4 Skip pypy3 tests on TravisCI 2016-07-02 12:03:52 +02:00
c8e06b55e1 Fix tests 2016-07-02 12:03:19 +02:00
5acbc904b7 Added the ability to unset headers
Closes #476
2016-07-02 11:50:30 +02:00
0c7c248dce Fix CHANGELOG 2016-07-02 11:17:38 +02:00
caf60cbc65 Typos 2016-07-02 11:11:06 +02:00
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
e25948f6a0 1.0.0-dev 2016-07-01 19:17:31 +02:00
b565b4628e v0.9.4 2016-07-01 19:02:34 +02:00
65081b2f12 Cleanup 2016-07-01 19:00:06 +02:00
963b2746f5 Be more liberal when detecting JSON in the formatter
Closes #485
2016-07-01 18:57:13 +02:00
098257c0be Rename --print-others to --history-print. 2016-07-01 18:49:27 +02:00
30eb0c2f26 Merge pull request #468 from Natim/master
Update readthedocs links.
2016-04-28 23:14:52 +08:00
9fbe745987 Update readthedocs links. 2016-04-28 12:28:20 +02:00
01a546eedd Merge pull request #463 from KyleAMathews/patch-1
Remove extra backtick.
2016-04-20 11:17:40 +08:00
eba6b63c55 Remove extra backtick. 2016-04-18 15:11:04 -07:00
ec245a1e80 added completions for fish shell 2016-04-06 11:28:03 -07:00
33eb9acd92 Updated README 2016-03-18 09:20:19 +08:00
293295cad6 Removed XML formatter
Closes #443
Closes #389
Closes #415
Closes #384
Closes #394
2016-03-18 09:16:39 +08:00
557911b606 Handle that os.pathconf is posix-only 2016-03-17 16:14:14 +08:00
5300b0b490 Fixed #451 - OSError: [Errno 36] File name too long 2016-03-17 15:58:01 +08:00
001bda1945 README 2016-03-17 15:00:50 +08:00
7c68d87c10 README 2016-03-10 14:27:33 +08:00
35a99fe04b Added test for -F shortcut 2016-03-09 21:58:34 +08:00
76e15b227c Added test_verbose_implies_all 2016-03-09 21:58:11 +08:00
8881ebf033 Changed the version icon label to include to word "stable" 2016-03-09 13:49:00 +08:00
25d1e8e418 Add Accept-Encoding: identity for --download
#423
2016-03-07 11:46:59 +08:00
7ce6eb148e Typo 2016-03-07 07:09:58 +08:00
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
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
5e03aeceb7 Make fruity default style one Windows
(again)
2016-03-06 08:33:40 +08:00
13ee9389aa Add link to contributors 2016-03-05 19:48:35 +08:00
bb49a1f979 Improved --debug output 2016-03-05 01:42:48 +08:00
4e574e6b8e Cleanup tests 2016-03-03 18:50:18 +08:00
529981af7a Fix CHANGELOG 2016-03-03 18:46:58 +08:00
6731cb881a README 2016-03-03 17:26:47 +08:00
f7d1b739e2 README 2016-03-03 17:24:46 +08:00
5bdf4a3bae Fixed test_rst_file_syntax error message 2016-03-03 17:22:12 +08:00
2d9414d34c Fixed README 2016-03-03 17:21:51 +08:00
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
5dbd104c3b Nobody ain't got time for that 2016-03-03 17:09:34 +08:00
13a979ad11 Cleanup 2016-03-02 14:20:35 +08:00
4cfa143bfe Fixed coverage 2016-03-02 13:31:40 +08:00
d24f30d0af Cleanup 2016-03-02 13:31:23 +08:00
66e168b2af Improved failed test output 2016-03-02 13:16:41 +08:00
564670566c Fix coveralls integration 2016-03-02 12:25:19 +08:00
ecbbad816a Fix coveralls integration 2016-03-02 12:24:46 +08:00
0432694661 Changel 2016-03-02 12:24:24 +08:00
dc4da527db Added --ssl=<PROTOCOL_VERSION>
Closes #98
2016-03-02 12:12:05 +08:00
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
c73dcaf63d CI 2016-03-02 10:08:20 +08:00
fb85509e91 CI 2016-03-02 09:58:50 +08:00
a2dca1e3bb CI 2016-03-02 09:44:39 +08:00
c2dae62af0 Appveyor: added Python 3.5 build 2016-03-02 09:37:58 +08:00
ae7008ee96 Appveyor 2016-03-02 09:33:43 +08:00
f6824f7ade Cleanup 2016-03-02 02:53:23 +08:00
7fd46e0b0d Cleanup 2016-03-02 01:02:11 +08:00
d4067fcb6d Added a short timeout for test requests 2016-03-02 00:31:00 +08:00
20f01709ea Mention URL escaping
Closes #311
2016-03-01 23:48:13 +08:00
56afd1adb9 Test suite cleanup 2016-03-01 23:22:50 +08:00
5e87a2d7e5 Cleanup 2016-03-01 23:13:45 +08:00
d30e28c2c7 Test suite improvements 2016-03-01 23:11:06 +08:00
0d2d24eac7 Copy 2016-03-01 21:37:26 +08:00
e2751e5fa3 Fixed args for Python 2.x 2016-03-01 21:28:10 +08:00
2a25d71aa4 Refactored main() into program() + main() 2016-03-01 21:10:54 +08:00
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
4f8d6c013b Fixed get_lexer() 2016-03-01 16:55:12 +08:00
e83e554ffb README 2016-03-01 16:50:30 +08:00
345f5a02a2 Fixed json absolute import 2016-03-01 16:39:50 +08:00
f96f0ef9ed JSON detection improvements 2016-03-01 16:22:54 +08:00
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
0fc1f61f3d Fixed README 2016-03-01 00:45:54 +08:00
c50413a9c1 Added support section 2016-03-01 00:24:52 +08:00
9f8c452e7e Added gitter chat 2016-03-01 00:12:55 +08:00
776328c818 Added gitter chat 2016-03-01 00:08:07 +08:00
9312fabc01 Capitalization II 2016-02-29 22:03:08 +08:00
48ce934dfa Capitalization 2016-02-29 22:01:05 +08:00
3625bb6fa1 Updated travis badge title 2016-02-29 22:00:18 +08:00
a97f0d52f6 Travis 2016-02-29 17:20:50 +08:00
41b0286f37 Travis 2016-02-29 17:19:12 +08:00
fee54b04d8 Travis 2016-02-29 17:09:55 +08:00
73e0455896 Travis 2016-02-29 16:52:33 +08:00
3b217daddc Travis 2016-02-29 16:43:18 +08:00
e5e5d0ce6d Travis 2016-02-29 16:36:25 +08:00
f43e473de1 Travis 2016-02-29 16:35:09 +08:00
0a002ec554 Fix travis 2016-02-29 15:38:59 +08:00
576ee83d82 Fix travis 2016-02-29 15:35:56 +08:00
e42f7b8fc9 OSX build 2016-02-29 15:32:10 +08:00
b44e16ed0f Fix appveyor 2016-02-29 15:14:43 +08:00
ed08ab133e Refactoring 2016-02-29 15:00:17 +08:00
5408fb0fb9 Cleanup 2016-02-29 14:31:27 +08:00
e18b609ef7 Fixed --max-redirects 2016-02-29 14:21:25 +08:00
356e043651 Added --show-redirects and --max-redirects
Closes #157, #183, #188, #246
2016-02-29 14:12:09 +08:00
c6d4f6cdf6 Show redirects WIP 2016-02-29 14:07:08 +08:00
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
e2235e56dc Update CHANGELOG.rst 2016-02-29 02:38:09 +08:00
763935b77f Update CHANGELOG.rst 2016-02-29 02:37:36 +08:00
6435532f72 Updated CHANGELOG 2016-02-28 21:37:49 +08:00
11a37067e7 Document -A as a short name for --auth-type 2016-02-28 21:01:21 +08:00
25f0156502 Merge pull request #432 from hangtwenty/master
Add `-A` as short arg for `--auth-type`
2016-02-28 20:58:23 +08:00
0f8d04b4df More robust mime type parsing
Closes #344
2016-02-28 20:12:16 +08:00
e385ed6a99 Fix README 2016-02-28 19:32:19 +08:00
01fdab55e9 Explain how to send fieldnames and headers starting with '-'
Closes #355
2016-02-28 19:31:43 +08:00
1127557742 Cleanup 2016-02-28 19:15:35 +08:00
5898879395 Fixed --download with --session
Closes #412
2016-02-28 19:14:10 +08:00
8c33e5e3d3 Parser => HTTPieArgumentParser 2016-02-28 19:01:54 +08:00
10da7b63a3 Mention MacPorts installation method
Closes #395
2016-02-28 16:54:33 +08:00
df193a373f Updated tarball URL 2016-02-28 16:20:19 +08:00
c2f8c36952 Updated download example URLs 2016-02-28 16:19:18 +08:00
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
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
d32d6f29a9 Typo 2016-02-17 14:53:00 +08:00
274dddfb45 Changed the default color style back to solarized
Closes #440
2016-02-17 14:46:35 +08:00
deb7b747cc Small fix for Python 2.6 compatibility.
Relates to #430 / #432.
2016-01-22 18:46:36 -06:00
018e1f68de Merge remote-tracking branch 'upstream/master' 2016-01-22 18:40:53 -06:00
ac69d4311b add -A as short arg for --auth-type
Addresses #430
comes with unit test
2016-01-22 18:37:30 -06:00
c75c4fa2a6 Merge pull request #429 from pra85/patch-1
Update license year range to 2016
2016-01-18 11:26:27 -03:00
a6a79e92e4 Update license year range to 2016 2016-01-18 11:20:24 +05:30
ea76542150 Added guardian/httpie-hmac-auth 2016-01-15 14:07:41 -03:00
c6690e0182 Makefile improvements 2016-01-02 14:33:48 -03:00
c82c9f0ae4 Makefile improvements 2016-01-02 14:28:46 -03:00
84b81c00ea Fixed tox.ini and improved tests and CONTRIBUTING.txt 2016-01-02 14:07:00 -03:00
34c6958dc8 1.0.0-dev 2016-01-01 19:38:21 -03:00
4722076335 v0.9.3 2016-01-01 19:27:07 -03:00
f14a0ad37d Fix pytest configuration 2016-01-01 19:11:22 -03:00
4cadc1d4c0 Update pytest configuration
* Move it from tox.ini to pytest.ini
* Ignore tests/fixtures
2016-01-01 19:08:27 -03:00
c3e5456aba Update setuptools on Appveyor 2016-01-01 19:01:39 -03:00
33489c9a91 Update pip on Appveyor 2016-01-01 18:51:17 -03:00
4e2b6b0ccc Update get-pip.py URL 2016-01-01 18:46:01 -03:00
b034c8703a PEP8 2016-01-01 18:41:58 -03:00
ab3d2656af Undo 'Fix "mock requires setuptools>=17.1. Aborting installation" on Win+Py3' 2016-01-01 18:38:06 -03:00
c42bd0051a Merge pull request #396 from pathcl/master
PEP8 errors
2016-01-01 18:37:49 -03:00
288cb4fdeb Fix "mock requires setuptools>=17.1. Aborting installation" on Win+Py3 2016-01-01 18:27:43 -03:00
8771d759fe Merge pull request #382 from konopski/master
[#381] Fixed --auth prompt on Windows
2016-01-01 18:18:23 -03:00
2cdca36960 Merge pull request #386 from honorabrutroll/dev
Fixed --pretty on Windows (closes #372)
2016-01-01 18:15:50 -03:00
8dc4f04fda Merge pull request #419 from hangtwenty/master
Fail gracefully if disable_warnings not available
2016-01-01 17:58:23 -03:00
dadc0cd27c Merge pull request #417 from yansal/patch-1
Fix typo in CONTRIBUTING.rst
2016-01-01 17:57:48 -03:00
59fd42244a Merge pull request #421 from Altreus/patch-1
Aitch already has an official spelling - use it
2016-01-01 17:56:52 -03:00
6afe9c32c4 Merge pull request #424 from t-mart/master
Remove duplicate setup.py test option
2015-12-28 13:56:14 -03:00
cc0ba03290 Remove duplicate setup.py test option 2015-12-28 10:38:07 -06:00
fad84a962e Aitch already has an official spelling - use it
http://www.oxforddictionaries.com/definition/english/aitch
2015-12-15 14:39:26 +00:00
4f755a8bde Fail gracefully if disable_warnings not available
Addresses #418. Rationale explained there.
2015-12-02 11:50:48 -06:00
21ee981fc6 Fix typo in CONTRIBUTING.rst 2015-11-27 15:02:23 +01:00
6259b5dd3b Add a --default-scheme argument. 2015-10-28 15:06:04 -07:00
45df860124 PEP8 errors 2015-10-22 14:32:16 -03:00
277da1ff93 str conversion 2015-10-21 21:57:06 +02:00
1ded5c2a97 Merge pull request #387 from yurimalheiros/patch-1
Update README.rst
2015-10-07 19:41:24 +02:00
69bd72ce95 Update README.rst
Fix a simple typo
2015-10-07 14:17:53 -03:00
8bf6db471b Fixed AUTHORS.rst 2015-10-04 17:16:41 -05:00
b1cc069fce Add Edward Yang to AUTHORS.rst 2015-10-04 17:06:54 -05:00
ed484c278b Change pretty option processor to only raise error when using output file 2015-10-04 17:06:00 -05:00
aec0f04f5d [#381] --auth fails on windows 2015-09-21 16:30:46 +02:00
8eb460a6f3 Disable OSX travis builds for now
OSX/Python support isn't probably not ready on Travis' side yet. 

https://travis-ci.org/jkbrzt/httpie/jobs/78219565#L27

// cc @msabramo — any idea?
2015-09-11 16:24:42 +02:00
5fe5958b06 Update README.rst 2015-09-01 15:11:36 +02:00
0e1c17daa1 Merge pull request #375 from hoatle/jwt-auth-plugin-reference
mention httpie-jwt-auth plugin on README
2015-09-01 15:10:41 +02:00
307517e7ef Merge pull request #377 from mblayman/fix-trasnsport
Fix trasnsport typo in plugin manager.
2015-08-31 06:20:01 +02:00
d60a04da2d Add Matt Layman to AUTHORS. 2015-08-30 21:41:14 -04:00
9ea89ffefe Fix typo in method name of plugin manager. 2015-08-30 21:37:47 -04:00
bebeb2100d mention httpie-jwt-auth plugin on README 2015-08-24 01:58:00 +07:00
2b51cb6687 Updated links II. 2015-07-03 18:55:45 +02:00
fa4bd033ef Updated links. 2015-07-03 03:40:38 +02:00
f8c1104429 Fixed link to httpie-edgegrid 2015-06-30 11:51:40 +02:00
be9d9281b7 Added a link to the httpie-edgegrid plugin. 2015-06-30 11:50:11 +02:00
ced0838598 Converted tabs to spaces. 2015-06-26 16:39:23 +02:00
d8b819b03f Merge pull request #337 from joaodelgado/json-serialization
Only serialize json if data is a dict instance
2015-04-24 15:07:16 +02:00
6fd0f23f39 Only serialize json if data is a dict instance 2015-04-11 02:11:22 +01:00
483546d781 Added mock to tests_require 2015-03-25 22:52:49 +01:00
daf3573908 Update CHANGELOG.rst 2015-03-25 22:37:48 +01:00
62407f781f Update CHANGELOG.rst 2015-03-25 22:35:36 +01:00
cbbaac13ea Merge pull request #300 from msabramo/print_info_about_request_on_error
Print info about request on error
2015-03-25 22:21:18 +01:00
6aad79d71c Merge pull request #319 from fay-jai/license
Update license with up-to-date year
2015-03-25 22:18:42 +01:00
c1f26347fc Merge pull request #330 from mihirvj/bash-completion
Bash auto completion
2015-03-25 22:08:13 +01:00
29a0147dd5 See #326
Adds bash completion to http command line interface.

Installing the script:
You can copy it to /etc/bash_completion.d/ (or something else on your
machine) and source it using following command

	$ source /etc/profile

Now whenever you encounter a "-*" on your CLI, it presents you with the
options specified.

Couple of things that are still under work:
1) Adding this bash script to setup, so that user won't need manual
installation
2) Adding more options for HTTP (GET, PUT and so on) and other
options
2015-03-24 22:26:10 -04:00
ab0d1fd8d0 Added .editorconfig. 2015-03-13 17:17:17 +01:00
35a3dd2855 Merge pull request #321 from ifdattic/patch-1
Fix typos, improve readability
2015-03-10 09:37:40 +01:00
ece85c0f0c Fix typos, improve readability 2015-03-10 10:05:13 +02:00
798cd4f0ec Update license with up-to-date year 2015-03-08 11:29:33 -07:00
1a43c0e5f7 Fixed --debug output 2015-02-28 17:02:05 +01:00
fdabbc6048 Typo 2015-02-24 16:50:02 +01:00
5f3de558cb README 2015-02-24 16:41:34 +01:00
fdae686e12 Clean up compat and fix is_pypy. 2015-02-24 08:18:03 +01:00
1c181a5d25 1.0.0-dev 2015-02-24 07:52:34 +01:00
a228399801 0.9.2 2015-02-24 07:50:15 +01:00
bada3b45f1 Use absolute links to LICENCE, etc. 2015-02-24 07:50:15 +01:00
e4bc363f9e Don't depend on requests.compat
#314
2015-02-24 07:50:15 +01:00
24957e3b61 Update requirements-dev.txt
dd7f1c4
2015-02-16 21:55:40 +01:00
fb437591da Include AUTHORS.rst in dist; metadata cleanup 2015-02-16 21:42:09 +01:00
b7fc89acdc README fixes 2015-02-16 21:29:40 +01:00
2e88aa53cf Extracted changes from README into a proper CHANGELOG file
Inspired by keepachangelog.com
2015-02-16 21:16:39 +01:00
9e62151bec Merge pull request #312 from msabramo/patch-5
tox.ini: Use pytest-httpbin>=0.0.6
2015-02-16 20:27:35 +01:00
ecc59591f1 Disable urllib3's "Unverified HTTPS request is being made" warnings 2015-02-16 19:36:02 +01:00
f855de16c2 Increase test coverage for error handling 2015-02-15 23:22:52 -08:00
7f8adad313 Print info about request on error
This can help in diagnosing certain issues. For example, if I were
trying to use a "http+unix" URL but I don't have #299, then I'll get the
following:

    [marca@marca-mac2 httpie]$ http http+unix://%2Ftmp%2Fprofilesvc.sock/status/pid

    http: error: ConnectionError: ('Connection aborted.', gaierror(8, 'nodename nor servname provided, or not known'))
    while doing GET request to URL: http://http+unix//%2Ftmp%2Fprofilesvc.sock/status/pid

Having the URL in the error message is super useful here so that I know an
extra `http://` is getting prepended and it's not doing what I expected.
2015-02-15 23:22:52 -08:00
51c19cfe10 test_ssl.py: Remove skip failures on PyPy
Revert 985f65e which skipped SSL tests that failed on PyPy because @kevin1024 fixed the problem in pytest-httpbin 0.0.6 (commit f38a312446)
2015-02-15 21:42:34 -08:00
dd7f1c4cce tox.ini: Use pytest-httpbin>=0.0.6
This hopefully fixes SSL timeout errors. 

Fixes #308
2015-02-15 20:33:57 -08:00
45784c7260 Fixed TOC 2015-02-15 12:57:57 +01:00
868baaba4e README 2015-02-15 12:53:57 +01:00
5760b780a0 README 2015-02-15 11:28:53 +01:00
2e5d14238f Tweak badge style 2015-02-15 00:54:49 +01:00
3b3eff01b7 Use shields.io badges 2015-02-15 00:51:58 +01:00
42f454eb6b README 2015-02-15 00:43:24 +01:00
40d95b650c README 2015-02-15 00:42:41 +01:00
bc0d17c04c Added a PyPy incompatibility workaround. 2015-02-15 00:36:55 +01:00
985f65ef52 Temporarily skip SSL tests on PyPy due to #308 2015-02-14 23:14:06 +01:00
dd0a4ab87a Default --style to "monokai"
419ca85
2015-02-14 22:51:31 +01:00
07aaefa232 Updated screenshot 2015-02-14 18:18:43 +01:00
419ca85e62 The default color --style is now "fruity"
It's experimental - please let me know should you dislike this change.

To make Solarized default again, add this to your ~/.config.json:

  "default_options": [
    "--style=solarized"
  ],
2015-02-14 18:18:04 +01:00
596fdc8c7e Update README examples with the new default Accept-Encoding value used by Requests. 2015-02-14 17:55:34 +01:00
6e7e2f2eea Changed the default JSON Content-Type to application/json. 2015-02-14 17:45:15 +01:00
748794257c Merge pull request #306 from msabramo/patch-4
.travis.yml: sudo false for Docker containers
2015-02-10 17:41:32 +01:00
55fa975ae5 .travis.yml: sudo false for Docker containers
Enables new Docker container infrastructure.
2015-02-10 07:41:38 -08:00
e6e94398ae Merge pull request #303 from msabramo/coveralls_only_one
.travis.yml: Only do coveralls on the latest Python version
2015-02-10 16:38:54 +01:00
fbd44640e6 .travis.yml: Only do coveralls on newest python
Testing theory that it has to do with different python version subjobs
completing in different orders and the last one wins.
2015-02-10 07:37:03 -08:00
43915b5fc0 Merge pull request #304 from msabramo/patch-2
compat.py: Add pragma no covers
2015-02-10 16:00:59 +01:00
f1e1299104 Merge pull request #305 from msabramo/patch-3
Conditionally skip test_session_unicode on Py3k
2015-02-10 16:00:24 +01:00
86ebb9b741 compat.py: Add pragma no covers
Cuz this is a lot of version-specific stuff and it can be confusing to have different coverage per version, especially with coveralls.
2015-02-10 06:54:59 -08:00
873102d5eb Mark test_session_unicode as xfail
There are known problems with unicode in headers.
See https://github.com/jakubroztocil/httpie/issues/282
2015-02-10 06:52:51 -08:00
337c05f95c README 2015-02-07 18:06:49 +01:00
a786f17997 1.0.0-dev 2015-02-07 17:04:33 +01:00
753a8d04e4 v0.9.1 2015-02-07 17:04:13 +01:00
3ff03524ff HTTP/2 has no minor versions.
https://github.com/jakubroztocil/httpie-http2/issues/1
2015-02-07 16:31:42 +01:00
a5a83c5b77 Prevent a circular import issue. 2015-02-07 16:29:17 +01:00
9682f955b5 Handle HTTP/2 responses
https://github.com/jakubroztocil/httpie-http2/issues/1#issuecomment-73301801
2015-02-06 21:13:57 +01:00
0d21ff022e Added a link to @pd's httpie-api-auth plugin 2015-02-06 20:06:50 +01:00
996e314482 Cleanup 2015-02-05 15:55:20 +01:00
687a6a734d Added support for transport adapter plugins
#276, #298
2015-02-05 15:25:00 +01:00
b125ce5eae Allow custom URL schemes
Closes #299

See also #276
2015-02-05 14:35:34 +01:00
92a4352f10 Added a coveralls badge. 2015-01-31 17:49:48 +01:00
c0f1fb61ac Merge pull request #297 from msabramo/patch-1
README.rst: suppor => support
2015-01-31 16:15:42 +01:00
17358be1ae README.rst: suppor => support 2015-01-31 07:01:54 -08:00
338d39c841 Fixed version link 2015-01-31 13:23:52 +01:00
530d6c5e27 1.0.0-dev 2015-01-31 13:22:17 +01:00
6c66d91f59 v0.0.9 2015-01-31 13:21:45 +01:00
ed6485498b README 2015-01-24 00:41:22 +01:00
59b6020105 Extended SSL documentation. 2015-01-24 00:22:31 +01:00
12f2d99bfd Added test client SSL certs 2015-01-23 23:56:08 +01:00
5fbafc18bc Added tests for client as well as server SSL certificate handling. 2015-01-23 23:55:03 +01:00
df07927843 --certkey is now --cert-key 2015-01-23 23:54:27 +01:00
d3d78afb6a Pypy3 (2.4.0) curses bug workaround. 2015-01-23 22:19:02 +01:00
25b1be7c8a Work around missing object_pairs_hook in Python 2.6 2015-01-23 22:04:42 +01:00
22c993bab8 Merge branch 'fix-268' of https://github.com/asnelzin/httpie into asnelzin-fix-268 2015-01-23 21:45:09 +01:00
b2ec4f797f Exit with 0 for --version and --help (closes #293). 2015-01-19 15:39:46 +01:00
a2b12f75ea Fixed and added test for JSON properties order. 2014-11-13 23:56:05 +03:00
0481957715 Fixed multiple uploads with the same field name
Closes #267
2014-10-20 14:41:48 +02:00
c301305a59 Cleanup. 2014-10-20 14:41:48 +02:00
2078ece95a Cleanup 2014-10-20 14:41:48 +02:00
43f7b84a1e Merge pull request #260 from brakhane/master
Fallback to JSON highlighting if subtype contains json
2014-09-25 06:27:17 +02:00
f1cd289d51 Fallback to JSON highlighting if subtype contains json
Some JSON based formats like JSON Home Documents[1] don't
use a '+json' suffix, but simply contain json in their
MIME type. Also, some servers might use (outdated)
types like 'application/x-json'.

The JSON formatter can already handle those cases,
but the highlighter was ignoring them.

This commit will let the highlighter choose the JSON
lexer if no other lexer could be found and the MIME subtype
contains 'json'

[1] http://tools.ietf.org/html/draft-nottingham-json-home-03
2014-09-25 00:10:06 +02:00
24f46ff3ef Changelog 2014-09-08 07:50:41 +02:00
afe521ef73 Merge remote-tracking branch 'origin/master' 2014-09-08 07:47:55 +02:00
58b51a8277 Improved terminal color depth detection via curses
Closes #244
2014-09-08 07:46:53 +02:00
6aa711c69f Removed pytest-xdist
The test suite is much less IO-bound now with the local httpbin
instance (via pytest-httpbin). Therefore, paralelization is not
as helpful.
2014-09-08 07:44:25 +02:00
d2d1023921 Merge pull request #249 from frewsxcv/patch-1
Enable testing on PyPy 3
2014-09-07 10:45:29 +02:00
b0effe07d9 Fixed --output=/dev/null on Linux
Closes #252
2014-09-07 10:22:21 +02:00
af873effb6 Changelog typo. 2014-09-05 18:40:28 +02:00
5084f18568 '\' only escapes separator characters in req-items
It makes easier to work with Windows paths.

Closes #253, #254
2014-09-05 18:36:23 +02:00
1035710956 Added RequestItems named tuple for convenience. 2014-09-05 07:51:35 +02:00
5d2b3f5552 Enable testing on PyPy 3 2014-08-15 00:03:27 -07:00
ca36f1de04 Handle empty passwords in URL credentials
Closes #242
2014-07-18 13:39:47 +02:00
0f96348fd1 Cleanup 2014-07-18 13:39:47 +02:00
2fd84ec1da Merge pull request #241 from ametaireau/patch-1
Add the hawk auth plugin
2014-07-17 07:58:08 +02:00
e3c83fca6f Add the hawk plugin 2014-07-17 00:48:56 +02:00
529f3bd9b6 Fixed python setup.py test 2014-06-28 19:52:10 +02:00
2a72ae23d5 Run tests against local httpbin instance via pytest-httpbin. 2014-06-28 16:38:41 +02:00
79329ed1c6 Mention "brew install httpie --HEAD". 2014-06-28 13:26:48 +02:00
040d981f00 Fixed custom Host
Closes #235
2014-06-28 13:24:14 +02:00
8c892edd4f PEP8 2014-06-28 13:09:04 +02:00
a02a1eb562 Fixed README formatting 2014-06-24 17:27:01 +02:00
5e556612d9 Added $ brew install httpie to README
https://twitter.com/jakubroztocil/status/481453834024550400

Thanks @insomniacslk!
2014-06-24 17:25:29 +02:00
f5904d92c3 Merge pull request #225 from rockymeza/docs_grep_fix
Fixed the order of args to grep in README.
2014-06-15 16:35:19 +02:00
541c75ed5c Fixed the order of args to grep in README. 2014-06-15 08:14:37 -06:00
8e170b059c Fixed tests. 2014-06-03 19:45:57 +02:00
b44bc0928f Merge pull request #222 from felixbuenemann/patch-1
Add info about SNI on Python 2.x to README
2014-05-26 15:37:11 +02:00
f283de6968 Add info about SNI on Python 2.x to README
This updates the HTTPS section of the README with instructions on how to get SNI working on Python 2.x.
2014-05-26 15:31:16 +02:00
77955c9837 Fixed --timeout
* Require requests >= 2.3.0
* Updated test_timeout_exit_status

Close #185.
2014-05-17 22:33:16 +02:00
4449da456a Merge pull request #220 from frewsxcv/patch-1
Add supported, relevant Python version classifers
2014-05-14 14:24:30 +02:00
f9b5b3a65d Added OSX to Travis CI config. 2014-05-14 14:00:26 +02:00
10f7fc163b Add supported, relevant Python version classifers 2014-05-12 17:36:09 -07:00
5743363ac9 Merge branch 'master' of github.com:jkbr/httpie 2014-05-12 19:16:15 +02:00
7036ec69ff Enable testing on Python 3.4 2014-05-12 19:16:04 +02:00
02c66e14df Update CONTRIBUTING.rst 2014-05-12 19:16:04 +02:00
ea8132b3d6 Update CONTRIBUTING.rst 2014-05-12 19:16:04 +02:00
e4c68063b9 Converted built-in formatters to formatter plugins.
Still work in progress and the API should be considered private for now.
2014-05-12 19:12:39 +02:00
9c2207844e Merge pull request #219 from frewsxcv/patch-1
Enable testing on Python 3.4
2014-05-12 08:02:17 +02:00
b51775bb06 Enable testing on Python 3.4 2014-05-11 20:09:47 -07:00
f26272f83f Update CONTRIBUTING.rst 2014-05-09 12:48:34 +01:00
81518f9315 Update CONTRIBUTING.rst 2014-05-09 12:46:33 +01:00
858555abb5 Make sure session and default headers play nice
Before: headers = default + args + session
Now:    headers = default + session + args

Fixes #180
2014-05-08 12:27:50 +01:00
3e1b62fb20 Fixed .rst syntax. 2014-05-05 21:17:41 +02:00
d9eca19b8f New URL. 2014-05-05 21:17:23 +02:00
5a989b6075 Fixed Makefile, added setup.cfg. 2014-04-28 13:27:02 +02:00
29a564ef56 Added wheel support
Should make installation via pip work on OSX Mavericks (#148).

Also added a nifty Makefile.
2014-04-28 13:25:47 +02:00
2aa53e4be3 Avoid “__init__.py” files in test directories.
As recommended here:

	https://pytest.org/latest/goodpractises.html
2014-04-28 11:29:41 +02:00
faec00fd99 Improve support for 'type/subtype+suffix' mime types in the colors output formatter.
E.g.:
* application/ld+json
* application/hal+json

Closes #189, #206
2014-04-28 10:08:03 +02:00
76ab8b84be Cleanup 2014-04-28 10:01:56 +02:00
14763e619d Travis coveralls. 2014-04-28 01:05:03 +02:00
0e6875bf83 Handle HTTP 0.9 in response when formatting version.
Closes #170
2014-04-28 00:08:20 +02:00
bd50a6adb1 Moved .directory from BaseConfigDict to Config.
Closes #200
2014-04-27 23:12:48 +02:00
f67a11c165 Debug appveyor 2014-04-27 22:20:23 +02:00
64b9a86c52 Debug appveyor 2014-04-27 22:15:21 +02:00
c8ae697eec Python 3.4 @ appveyor. 2014-04-27 22:14:11 +02:00
82e16c4f27 Debug appveyor 2014-04-27 22:10:24 +02:00
05db75bdb1 Modularized output, refactoring
Making it ready for output formatting plugin API.
2014-04-27 21:58:00 +02:00
c06598a0c4 Cleanup 2014-04-27 18:27:44 +02:00
18f3700b77 Fix appveyor.yml V. 2014-04-27 17:54:30 +02:00
d05063f019 Fix appveyor.yml IV. 2014-04-27 17:52:04 +02:00
7c3f8c021e Fix appveyor.yml III. 2014-04-27 17:50:54 +02:00
a95d8bb42d Fix appveyor.yml 2014-04-27 17:46:19 +02:00
411822d3b2 Fix appveyor.yml 2014-04-27 17:45:23 +02:00
bae8519e29 Python3.3 Windows CI 2014-04-27 17:38:12 +02:00
87806acc56 Cleanup 2014-04-26 23:06:39 +02:00
1169a3eb23 Fixed tests. 2014-04-26 20:14:46 +02:00
43bc6d0c98 Fixed and added tests for --verbose with unicode headers. 2014-04-26 20:10:15 +02:00
eca1ffaedb More unicode. 2014-04-26 19:47:14 +02:00
0bd218eab0 Cleanup 2014-04-26 19:32:08 +02:00
609950f327 Updated Travis icon URL. 2014-04-26 18:48:57 +02:00
bbc820bf2e Fixed fixture loading on Windows. 2014-04-26 18:41:28 +02:00
84a521a827 Added test_unicode_url_query_arg_item. 2014-04-26 18:23:13 +02:00
a3352af1d4 Added support and tests for unicode support in sessions. 2014-04-26 18:16:30 +02:00
e8a1c051f9 Changelog 2014-04-26 17:53:35 +02:00
3478cbd9ff More unicode tests. 2014-04-26 17:53:01 +02:00
77dcd6e919 Added unicode characters to json fixture. 2014-04-26 17:37:56 +02:00
467d126b6c Python 3 unicode fixes. 2014-04-26 17:35:26 +02:00
8ec32fe7f3 Fix tox config. 2014-04-26 16:50:31 +02:00
282cc455e3 Avoid "TypeError: keyword arguments must be strings" on Python 3.3. 2014-04-26 15:18:38 +02:00
56d33a8e51 Fix Windows branch. 2014-04-26 15:10:39 +02:00
15e62ad26d Implemented more robust unicode handling.
* Immediatelly convert all args from `bytes` to `str`.
* Added `Environment.stdin_encoding` and `Environment.stdout_encoding`
* Allow unicode characters in HTTP headers and basic auth credentials
  by encoding them using UTF8 instead of latin1 (#212).
2014-04-26 15:07:31 +02:00
5c29a4e551 Added windows build status icon to README. 2014-04-26 11:32:41 +02:00
0c45c7cb39 Disabled test_windows_colorized_output 2014-04-26 11:06:50 +02:00
8158fa8c45 Run tests in verbose mode. 2014-04-26 11:03:53 +02:00
5065c4f878 Updated appveyor.yml 2014-04-26 11:01:02 +02:00
e3af74da46 Don't used pytest-xdist with setup.py test 2014-04-26 10:59:46 +02:00
5c3d24ec09 Updated appveyor.yml 2014-04-26 10:49:40 +02:00
091a8b2692 Updated appveyor.yml 2014-04-26 10:46:08 +02:00
95a0884f95 Updated appveyor.yml 2014-04-26 10:41:57 +02:00
8fb1e106ee Updated appveyor.yml 2014-04-26 10:36:12 +02:00
78c83da721 Updated appveyor.yml 2014-04-26 10:33:13 +02:00
aeccac5cbd Updated appveyor.yml 2014-04-26 10:28:16 +02:00
e2dabbfaf7 Updated appveyor.yml 2014-04-26 10:26:29 +02:00
272e66bf37 Updated appveyor.yml 2014-04-26 10:22:17 +02:00
4a0d387f86 Updated appveyor.yml 2014-04-26 10:20:45 +02:00
6a86164510 Updated appveyor.yml 2014-04-26 10:14:57 +02:00
e1348da118 Updated appveyor.yml 2014-04-26 10:13:31 +02:00
0e1b651a1c Added appveyor.yml 2014-04-26 10:07:35 +02:00
631e332dad Cleanup 2014-04-25 13:57:33 +02:00
33422312c5 Cleanup 2014-04-25 13:52:43 +02:00
1d987c5b4d Improved session tests. 2014-04-25 13:50:44 +02:00
3c2de34285 Improved auth tests. 2014-04-25 13:10:01 +02:00
b10d973019 Removed unused import. 2014-04-25 12:53:02 +02:00
492ee392bd Cleanup 2014-04-25 12:42:50 +02:00
af4aa3a761 Test improvements. 2014-04-25 12:18:35 +02:00
27faf06327 Removed last dependencies on unittest. All tests are pytest-only. 2014-04-25 11:39:59 +02:00
f658d24c93 Parametrize test_docs.py. 2014-04-25 10:41:04 +02:00
ea42d32f69 Travis doesn't support Python 3.4 yet. 2014-04-25 09:47:35 +02:00
3f63133b7c Parallelized tests using pytest-xdist. 2014-04-24 21:36:03 +02:00
3f8a000847 Python 3.4 2014-04-24 20:08:28 +02:00
f02169ea71 Added Python 2.6 compatible OrderedDict
To preserver ordr of headers, parameters, etc.
2014-04-24 19:57:19 +02:00
e5d758e4ce More tests. 2014-04-24 19:32:55 +02:00
ce2169f4fe Added docstrings for utilities in tests.__init__. 2014-04-24 19:32:55 +02:00
bdea7be456 Added tests for --debug and --help. 2014-04-24 19:32:55 +02:00
887f70f595 Added CONTRIBUTING.rst. 2014-04-24 19:32:55 +02:00
3d079942f4 Finished pytest migration. 2014-04-24 19:32:55 +02:00
3cb124bba7 Cleanup
XX
2014-04-24 19:32:50 +02:00
6f28624134 Switched to @pytest.mark.skipif. 2014-04-24 15:17:23 +02:00
941c0a8c3c Moved fixture constants to tests.fixtures. 2014-04-24 15:17:04 +02:00
b880e996d0 Converted all unittest asserts to plain, pytest-powered asserts. 2014-04-24 14:58:15 +02:00
6071fff4af Refactored tests into smaller modules. 2014-04-24 14:07:31 +02:00
746a1899f3 Skip ExitStatusTest.test_timeout_exit_status until timeout gets fixed in requests. 2014-03-31 13:01:55 +02:00
bbbae3ae25 Fixed SessionTest.test_session_read_only. 2014-03-31 13:01:55 +02:00
e62620d4ad Merge pull request #208 from insyte/master
Update README.rst with pronunciation.
2014-03-25 10:19:32 +01:00
a2918d877d Update README.rst 2014-03-24 18:03:59 -05:00
733771fd9e Merge pull request #172 from unsignedint/master
process XML data before pretty-printing to trim whitespace
2014-03-18 19:44:16 +01:00
76ab6e49d5 Updated installation instructions. 2014-03-04 18:44:31 +01:00
c33775e785 Updated installation instructions. 2014-03-04 18:42:33 +01:00
09810d55ba Updated installation instructions. 2014-03-04 18:36:22 +01:00
29877bc8ad Updated installation instructions. 2014-03-04 18:24:32 +01:00
af6bda11af Removed Bitdeli badge. 2014-02-18 14:09:50 +01:00
b01906a45c Fixed ZeroDivisionError in download summary.
Closes #202
2014-02-18 13:06:18 +01:00
2c885b0981 Merge pull request #197 from matleh/master
add support for client SSL certificate and key
2014-02-12 14:46:59 +01:00
b3a34aba44 added --cert to CHANGELOG and matleh to AUTHORS 2014-02-12 11:23:31 +01:00
dd7197c60b document --cert and --certkey 2014-02-05 12:51:05 +01:00
a3aae12d9c rename -ssl-cert and --ssl-key to --cert and --certkey 2014-02-05 12:50:40 +01:00
d4363a560d rename existing_file to readable_file_arg and move to input 2014-01-29 18:02:06 +01:00
b9d7220b10 check --ssl-cert and --ssl-key to be files 2014-01-29 15:54:19 +01:00
14583a2efa add support for client SSL certificate and key 2014-01-28 16:16:48 +01:00
43cc3e7ddb Fixed changelog link. 2014-01-25 15:15:16 +01:00
f1224da526 v0.8.0 2014-01-25 15:11:38 +01:00
e0cc63c7eb Cleanup 2014-01-25 15:09:28 +01:00
52dd6adaa3 Updated README. 2014-01-25 15:04:15 +01:00
1aa77017d5 Catch UnicodeDecodeError when embedding file via =@ or :=@. 2014-01-25 14:57:19 +01:00
748a0a480d Update README.rst 2014-01-17 08:57:05 +01:00
01df344a07 Update README.rst 2014-01-17 08:56:24 +01:00
b1074ccb4f Merge pull request #191 from solidsnack/wip-no-auth-in-host-header
Expunge user:pass@... from Host header.
2014-01-08 02:28:19 -08:00
7a84163d1c Merge pull request #192 from thomasleveil/patch-1
fix typo
2014-01-08 02:27:29 -08:00
a31d552d1c fix typo 2014-01-07 14:04:13 +01:00
5a037b2e13 Expunge user:pass@... from Host header.
In verbose mode, the basic auth user and password would show up in colored
output reporting the Host header, as reported in
https://github.com/jkbr/httpie/issues/169
2014-01-06 19:12:33 +00:00
6af42b1827 Added Bitdeli badge. 2013-12-08 11:38:26 +01:00
bee10e5eed replace XML processor with ElementTree with custom indentation 2013-10-16 13:07:53 +13:00
bcdf194bae process XML data before pretty-printing to trim whitespace 2013-10-16 12:33:19 +13:00
0e267d8efa Added a link to the httpie-negotiate auth plugin by @ndzou II. 2013-10-09 23:46:55 +02:00
927acc283e Added a link to the httpie-negotiate auth plugin by @ndzou. 2013-10-09 23:44:55 +02:00
817165f5ff Merge pull request #171 from nlf/master
Allow :port style shorthand for localhost.
2013-10-09 13:22:30 -07:00
4fe3deb9d9 add self to authors, update changelog, and mention shorthand in --help output 2013-10-09 13:21:14 -07:00
9034546b80 tweak readme more 2013-10-09 11:37:05 -07:00
2c12fd99f9 tweak readme more 2013-10-09 11:36:01 -07:00
70eb97dece tweak readme to show http requests 2013-10-09 11:34:22 -07:00
8a52bef559 make shorthand parsing more robust, add unit tests and documentation 2013-10-09 11:32:41 -07:00
711168a899 allow :port style shorthand 2013-10-08 22:41:38 -07:00
81c99886fd Update --proxy examples to include URLs to work with Requests v2.0.0.. 2013-09-25 22:02:29 +02:00
2e535d8345 Fixed password prompt. 2013-09-25 00:17:50 +02:00
0bcd4d2fb0 Fixed a bytes/str issue for Python 3. 2013-09-25 00:00:17 +02:00
d5bc564e4f Allow embeding text (=@) and JSON (:=@) files content into request data fields. 2013-09-24 23:41:18 +02:00
54c5c3d82b 0.7.1 2013-09-24 21:57:29 +02:00
2a6514eb5d Update to requests 2.0.0
Closes #140.
2013-09-24 21:49:43 +02:00
22c2cc6465 Removed unused import. 2013-09-24 20:30:54 +02:00
2265edf05e Cleanup 2013-09-24 20:15:19 +02:00
87774acf5c Changelog 2013-09-24 20:09:23 +02:00
9d2ac5d8ad 0.7.0 2013-09-24 20:07:48 +02:00
3e4e1c72a4 Merge branch 'master' of github.com:jkbr/httpie 2013-09-24 19:51:06 +02:00
29f6b6a2a9 Improved Content-Disposition parsing for --download mode
Closes #168.
2013-09-24 19:50:37 +02:00
26b2d408e7 Merge pull request #167 from matt-hickford/master
Fix plugins ImportError
2013-09-23 02:13:14 -07:00
b5f180a5ee Fix plugins ImportError described at https://github.com/jkbr/httpie/issues/166#issuecomment-24905910 2013-09-23 09:54:06 +01:00
354aaa94bd Improved .netrc example formatting. 2013-09-22 15:20:50 +02:00
2ad4059f92 Improved .netrc example formatting. 2013-09-22 15:19:59 +02:00
5a6b65ecc6 Added link to httpie-oauth. 2013-09-22 15:10:50 +02:00
2acb303552 Added support for auth plugins. 2013-09-21 23:46:15 +02:00
f7b703b4bf Added --ignore-stdin
Closes #150
2013-08-23 10:57:17 +02:00
00de49f4c3 Cleanup 2013-08-18 00:59:10 +02:00
67496162fa Improved --help output. 2013-08-10 11:56:19 +02:00
8378ad3624 Try to import argparse before adding it to reqs. 2013-08-01 09:07:33 +02:00
f87884dd8d README 2013-08-01 08:46:37 +02:00
b671ee35e7 Merge pull request #153 from lorin/patch-1
Augment cookie example in README for multiple cookies
2013-07-31 07:52:22 -07:00
69247066dc Augment cookie example in README for multiple cookies
This change updates the README to show how to pass multiple cookies.
2013-07-31 10:29:38 -04:00
383dba524a Print error when download is interrupted by server
Close #147
2013-07-07 17:00:03 +02:00
60f09776a5 httpless outputs also response headers by default 2013-06-03 12:28:04 +02:00
48719aa70e README 2013-06-03 12:22:34 +02:00
809a461a26 v0.6.0 2013-06-03 12:19:43 +02:00
c3d550e930 Fixed headers tests; Require requests>=1.2.3. 2013-06-02 20:47:29 +02:00
172df162b3 Added XML formatting to CHANGELOG. 2013-06-02 20:27:58 +02:00
1bad62ab0e Handle unicode when formatting XML. 2013-06-02 20:25:36 +02:00
8d302f91f9 Merge branch 'master' of git://github.com/jargonjustin/httpie into jargonjustin-master 2013-06-02 20:14:51 +02:00
63b61bc811 Add custom Host example. 2013-05-20 15:31:02 +02:00
5af88756a6 Fixed download ETA for Python 2.6. 2013-05-14 12:49:29 +02:00
7f624e61b5 Use Thread instead of Timer for progress reporting. 2013-05-14 12:49:03 +02:00
6e848b3203 cleanup 2013-05-14 12:14:08 +02:00
8e112a6948 test_download_no_Content_Length 2013-05-13 15:35:12 +02:00
87c59ae561 Added anonymous sessions (--session=/file/path.json). 2013-05-13 14:47:44 +02:00
76eebeac2a 0.6.0-dev 2013-05-13 12:42:16 +02:00
5b9cbcb530 v0.5.1 2013-05-13 12:40:25 +02:00
8ad33d5f6a Changelog 2013-05-13 12:20:54 +02:00
86ac4cdb7b Changelog 2013-05-13 12:20:28 +02:00
e09b74021c Ignore Content-* and If-* request headers.
Those headers are not stored in sessions anymore.

Closes #141.
2013-05-13 11:54:49 +02:00
71e7061014 v0.5.0 2013-04-27 12:03:38 -03:00
bc756cb6a2 Cleanup 2013-04-27 11:57:13 -03:00
63ed4d32a7 Merge remote-tracking branch 'origin/master' 2013-04-17 13:52:02 -03:00
d1b91bfa9c Merge pull request #142 from capncodewash/netrc-example
Added example for .netrc usage (closes #139)
2013-04-17 09:46:50 -07:00
dac79a8efc Added example for .netrc usage (see issue #139 in upstream. 2013-04-17 16:32:55 +01:00
1fc8396c4b Stop the progres reporter thread on error. 2013-04-16 04:55:45 -03:00
6c3b983c18 Tests 2013-04-15 00:56:47 -03:00
cfa7199f0b Added a simple download test. 2013-04-13 15:34:31 -03:00
5a1177d57e Fixed downloads with no Content-Length. 2013-04-13 14:50:46 -03:00
c63a92f9b7 Cleanup 2013-04-12 22:02:34 -03:00
d17e02792b Fixed length progress bar. 2013-04-12 21:49:27 -03:00
fc4f70a900 Colorize stderr on Windows. 2013-04-12 17:15:21 -03:00
1681a4ddd0 TODOs 2013-04-12 15:27:26 -03:00
289e9b844e Fixed Content-Type retrieval for Python 3. 2013-04-12 14:07:21 -03:00
72cf7c2cb7 Fixed tests for Python 2.6. 2013-04-12 13:42:34 -03:00
4d84d77851 Cleanup 2013-04-12 13:09:57 -03:00
1b98505537 Validate download options before setting up streams. 2013-04-12 11:59:23 -03:00
d32acfe2fa Only use Range when already have a partial download. 2013-04-12 11:56:05 -03:00
e8d79c4d8c Docs fix. 2013-04-12 11:37:58 -03:00
38206e9e92 Cleanup 2013-04-12 11:26:42 -03:00
55d5e78324 --download docs (#104). 2013-04-12 11:06:03 -03:00
341272db1e Added support for output redirection with --download (#104). 2013-04-12 11:04:14 -03:00
464b7a36da Tests 2013-04-12 10:20:01 -03:00
9d043eb745 Used Content-Disposition filename (#104). 2013-04-12 10:19:49 -03:00
40bd8f65af Handle KeyboardInterrupt while --download'ing (#104). 2013-04-12 09:08:19 -03:00
347653b369 Performance and progress bar improvements.
#104
2013-04-12 08:59:33 -03:00
ebfce6fb93 Improved progress bar (#104). 2013-04-11 18:51:21 -03:00
674acfe2c2 Cleanup 2013-04-11 16:23:15 -03:00
7ccdece39f Cleanup 2013-04-11 04:00:41 -03:00
e53dcba03e Added Content-Range parsing tests.
#104
2013-04-11 03:49:01 -03:00
486657afa3 Improved Content-Range parsing.
#104
2013-04-11 03:24:59 -03:00
599bc0519f Download resume improvements.
- Set correct Range
- Validate respnse status
- Validate Content-Range

 #104
2013-04-11 02:29:10 -03:00
21613faa5a Progress bar update 2013-04-10 13:07:05 -03:00
36bc64e02f Cleanup. 2013-04-10 12:53:25 -03:00
6e5c696ac9 --json with no data sets Content-Type as well
Closes #137
2013-04-02 11:07:14 -03:00
9b2a293e6e Progress on --download. 2013-03-24 11:23:18 -03:00
b0dd463687 Corrected session info in the README. 2013-03-22 16:26:51 -03:00
bffaee13ff Formatting 2013-03-20 12:07:23 -03:00
30afcea72d Merge pull request #135 from Scorpil/master
Fixed PyPy cookie updating issue

Closes #132
2013-03-20 08:05:23 -07:00
631c54b711 Fixed PyPy cookie updating issue 2013-03-20 11:45:56 +02:00
99f82bbd32 Handle downloads with no Content-Length. 2013-03-07 13:32:48 -03:00
6f64b437b7 Fixed streaming (closes #133) 2013-03-07 12:42:29 -03:00
7774eac3df Fixed unique suffix placement for URLs with a file extension. 2013-03-03 22:35:01 -03:00
8e6c765be2 Initial --download implementation (#104).
Closes #127
2013-03-03 22:17:09 -03:00
f0c42cd089 v0.4.1 2013-02-26 14:37:09 +01:00
5c6cea79a1 Removed a reference to the removed httpie command
Closes #131
2013-02-26 14:31:52 +01:00
2bed81059a Updated README. 2013-02-22 14:04:27 +01:00
be0b2f21d2 v0.4.0 2013-02-22 13:52:50 +01:00
d97a610f7c Added new logo by @claudiatd 2013-02-22 13:51:37 +01:00
5cc5b13555 Removed the management command.
It means that:

    httpie session list
    httpie session edit
    ...

are gone.

It has never been part of a stable release, and since it wasn't
a very useful feature, it's beeing removed now to avoid feature creep.
2013-02-22 13:27:26 +01:00
3043f24733 .gitignore 2013-02-22 13:19:18 +01:00
093dab5896 Multiple headers TODO. 2013-02-22 13:18:18 +01:00
5f42a21cfb Simplified stored session cookie data. 2013-01-22 20:03:28 +01:00
4c45f0d91f Session name escaping. 2013-01-22 20:02:39 +01:00
d7ec7b2217 Fixing tests for Travis. 2013-01-04 03:19:38 +01:00
7817dfbbcc Fixing tests for Travis. 2013-01-04 03:09:21 +01:00
238b2e0441 Fixing tests for Travis. 2013-01-04 03:05:36 +01:00
a93d57b58b Fixed request/response session cookies.
Closes #113.
2013-01-04 02:59:05 +01:00
79c412064a Python 3.3 fixes. 2013-01-03 15:19:21 +01:00
0ae9d7af58 Compatibility with requests v1.0.4 (requests URL params). 2013-01-03 14:42:17 +01:00
80e317fe24 Added Python 3.3 to tox and travis conf. 2013-01-03 14:14:22 +01:00
1481749c22 Use urlsplit instead of urlparse.
Closes #118.
2013-01-03 14:12:27 +01:00
d84d94dd55 Clean up 2013-01-03 13:49:41 +01:00
1913b0d438 Merge branch 'master' of github.com:jkbr/httpie 2012-12-19 12:31:34 +01:00
fe16f425a9 Require Requests v1.0.3. 2012-12-19 12:31:01 +01:00
7ff71a7f10 Revert: Test Python 3.3 on Travis.
3.3 still not supported
2012-12-19 11:56:02 +01:00
4a37d10245 Test Python 3.3 on Travis. 2012-12-19 11:53:26 +01:00
e5edb66ae8 Requests v1.0: Fixed request body access. 2012-12-19 11:37:52 +01:00
2e57c080fd Pretty print XML 2012-12-17 13:21:38 -08:00
1766dd8291 Requests 1.0: session cookies. 2012-12-17 17:18:18 +01:00
675a8b17ad Merge branch 'master' of github.com:jkbr/httpie 2012-12-17 17:14:24 +01:00
69e26b8bc8 Requests 1.0: prefetch; default_headers. 2012-12-17 17:02:27 +01:00
291f520e0c Update README.rst 2012-12-17 12:26:57 +01:00
9ec328ff6f Session commands. 2012-12-11 12:54:34 +01:00
f2d59ba6bd Improved --check-status + HTTP error + stdout redirect warning. 2012-12-05 05:27:11 +01:00
53caf6ae72 Cleanup 2012-12-05 05:06:06 +01:00
8175366f27 PEP8 2012-12-05 04:39:56 +01:00
8190a7c0c6 Fixed httpie session list 2012-12-05 04:36:42 +01:00
4a615e762f Updated session docs. 2012-12-01 18:43:33 +01:00
7426b4b493 RST formatting. 2012-12-01 18:26:15 +01:00
2cdcadd9d5 Added docs for httpie. 2012-12-01 18:25:34 +01:00
18510a9396 Progress on httpie session *. 2012-12-01 18:16:00 +01:00
acf5f063c7 Typo 2012-12-01 16:52:23 +01:00
2cf379df78 Fixed README typo. 2012-12-01 16:20:16 +01:00
dd100c2cc4 Fixed -j & -v & redirected stdout. Closes #109. 2012-12-01 15:55:58 +01:00
444a9fa929 Added httpless to README. 2012-12-01 15:54:36 +01:00
4a24cd25b9 Clean up. 2012-12-01 15:20:14 +01:00
1c5fb89001 Output stream refactoring. 2012-11-09 15:49:23 +01:00
466e1dbedf Updated CHANGELOG (#100). 2012-11-08 22:39:28 +01:00
d87b2aa0e5 Added support for credentials in URL.
Closes #100 🍰
2012-11-08 22:29:54 +01:00
5d969852c7 Added --no-option's and made args more config-friendly. 2012-09-24 06:49:12 +02:00
bbc702fa11 Improved README. 2012-09-24 05:59:52 +02:00
e25d64a610 0.3.0 2012-09-21 05:50:01 +02:00
a41dd7ac6d Allow output redirection on Windows.
Closes #88.
2012-09-21 04:30:59 +02:00
4a6f32a0f4 Documented config.
Also renamed `default_content_type` to `implicit_content_type` .
2012-09-17 03:08:45 +02:00
548bef7dff Added tests for sessions. 2012-09-17 02:15:00 +02:00
6c2001d1f5 Use the HTTPIE_CONFIG_DIR environment variable. 2012-09-17 02:12:16 +02:00
4029dbf309 Added configuration file.
The "default_content_type" option can be set to "form".

Closes #91.
2012-09-17 00:37:36 +02:00
478d654945 Renamed --session-read to --session-read-only. 2012-09-17 00:01:49 +02:00
66bdbc3745 Cleanup. 2012-09-07 12:48:59 +02:00
316e3f45a9 Added --session-read for read-only sessions. 2012-09-07 12:38:52 +02:00
da0eb7db79 Renamed --allow-redirects to --follow. 2012-09-07 11:58:39 +02:00
9338aadd75 Cleanup 2012-09-05 20:22:08 +02:00
dc7d03e6b8 Merge pull request #90 from simonbuchan/898408c20cfab130699cee3bedbae1ad4a1c69b1
Fix --session for Windows (with a Requests patch)
2012-09-04 02:38:34 -07:00
898408c20c Fix sessions for Windows
':' is invalid in a Windows path, and json needs output to support
write(str).
2012-09-04 15:53:27 +12:00
47de4e2c9c Sessions are now host-bound. 2012-08-19 04:58:14 +02:00
f74424ef03 README 2012-08-18 23:11:56 +02:00
8a9cedb16e JSON session data, `httpie' management command. 2012-08-18 23:07:36 +02:00
ff9f23da5b Grouped arguments for a more user-friendly --help. 2012-08-18 06:12:44 +02:00
50810e5bd9 Include data directory location with --debug. 2012-08-18 04:45:29 +02:00
9b586b953b Use %APPDATA% for data on Windows. 2012-08-18 04:36:58 +02:00
149cbc1604 Fixed Solarized style unavailable on Windows.
#87.
2012-08-18 03:54:38 +02:00
d3df59c8af Updated README. 2012-08-17 23:35:36 +02:00
2057e13a1d Updated README. 2012-08-17 23:35:06 +02:00
4957686bcd Updated README. 2012-08-17 23:34:42 +02:00
4c0d7d526f Added initial support for persistent sessions. 2012-08-17 23:30:47 +02:00
0b3bad9c81 Added initial support for persistent sessions. 2012-08-17 23:23:02 +02:00
1ed43c1a1e Semver-compatible versioning. 2012-08-17 21:24:34 +02:00
bf03937f06 Unified output processing options under --pretty.
* --pretty=none instead of --ugly
* --pretty=all instead of --pretty
* --pretty=colors instead of --colors
* --pretty=format instead of --format
2012-08-17 21:15:37 +02:00
4660da949f Fixed colorized output on Windows with Python 3.
Closes #87.
2012-08-17 06:35:18 +02:00
86256af1df Removed non-ASCII characters from README (closes #85). 2012-08-16 18:47:30 +02:00
8bf7f8219c Fixed readme decoding.
Closes #85.
2012-08-16 03:11:15 +02:00
a5522b8233 Revert "Iter body lines to avoid binary false positives."
This reverts commit b92a3a6d95.
2012-08-16 03:06:48 +02:00
b92a3a6d95 Iter body lines to avoid binary false positives.
#84
2012-08-13 23:33:25 +02:00
9098e5b6e8 Updated changelog. 2012-08-12 06:02:13 +02:00
68640a81b3 Use CRLF for headers in the output. 2012-08-10 01:45:07 +02:00
27f08920c4 Improved examples. 2012-08-09 23:36:29 +02:00
c01dd8d64a Added exit status for timed-out requests. 2012-08-09 05:24:58 +02:00
76feea2f68 Added README reStructuredText validation. 2012-08-07 17:20:50 +02:00
22a10aec4a Added --colors and --format.
Closes #59 and #82.
2012-08-07 16:59:49 +02:00
fa334bdf4d Documented --verify. 2012-08-07 15:25:24 +02:00
f6724452cf Skip tests with redirects on Requests 0.13.6. 2012-08-07 15:08:28 +02:00
07de32c406 Version fix. 2012-08-07 15:01:04 +02:00
1fbe7a6121 Improved --debug. 2012-08-07 14:50:51 +02:00
49e44d9b7e Pre-process README.rst so that PyPi renders it. 2012-08-07 14:50:17 +02:00
193683afbb Added proxy docs. 2012-08-07 14:49:43 +02:00
126b1da515 v0.2.8dev 2012-08-07 00:13:27 +02:00
969b310ea9 v0.2.7 2012-08-07 00:12:47 +02:00
dd2c89412c Compatibility with Requests 0.13.6. 2012-08-07 00:07:04 +02:00
381e60f9d8 Extended README. 2012-08-06 23:27:49 +02:00
44e409693b Set JSON Content-Type only with data even with -j. 2012-08-06 22:14:52 +02:00
4e58a3849a Added exit status constants, cleaned up main(). 2012-08-04 19:22:50 +02:00
94c77c9bfc Improved password prompt. 2012-08-04 17:04:36 +02:00
747b87c4e6 Changelog, typos 2012-08-04 16:46:39 +02:00
c7657e3c4b Streamed terminal output
`--stream` can be used to enable streaming also with `--pretty` and to ensure
a more frequent output flushing.
2012-08-04 16:35:31 +02:00
4615011f2e Sort headers by name when prettifying. 2012-08-03 00:58:01 +02:00
4b1a04e5ed Fixed error handling. 2012-08-02 04:33:43 +02:00
e045ca6bd8 Cleanup, CHANGELOG 2012-08-01 23:51:30 +02:00
52e46bedda Take advantage of streaming.
It's now possible to download huge files with HTTPie, and it's often faster than curl and wget!
2012-08-01 23:21:52 +02:00
67ad5980b2 Don't fetch the response body unless needed.
E.g., this will only read the response headers but won't download the
whole file:

    http GET --headers example.org/big-file.avi

The request method is respected (i.e., it doesn't switch to HEAD like
cURL does).
2012-08-01 21:31:06 +02:00
00d85a4b97 Fallback to media subtype if the type is uknown.
Closes #81.
2012-08-01 17:37:23 +02:00
90d34ffd0d Added tests for binary request data. 2012-08-01 00:52:30 +02:00
8905b4fc72 cleanup 2012-07-30 14:23:22 +02:00
a5b98818c8 Syntax-highlighting for examples in the README. 2012-07-30 13:58:13 +02:00
5e7bb1f6dc Syntax-highlighting for examples in the README. 2012-07-30 13:51:28 +02:00
4117d99dd0 Updated screenshot. 2012-07-30 12:37:59 +02:00
49604e7c29 Updated screenshot. 2012-07-30 12:29:56 +02:00
72d371c467 Updated screenshot. 2012-07-30 12:24:11 +02:00
a8c9441f71 Updated screenshot. 2012-07-30 12:11:28 +02:00
e13f65ace1 Updated solarized and switched to Solarized256Style. 2012-07-30 12:11:16 +02:00
a1682d0d2e Added AUTHORS 2012-07-30 12:10:19 +02:00
923a8b71bd Revorked output
Binary now works everywhere. Also added `--output FILE` for Windows.
2012-07-30 10:58:16 +02:00
6eed0d92eb Better error messages. 2012-07-29 07:14:54 +02:00
edf87c3392 Consistent request-response separators. 2012-07-29 06:59:51 +02:00
f73bfea6b8 Validate "file fields (name@/path) require --form / -f". 2012-07-29 06:58:50 +02:00
16635870e3 Removed redundant decode/encode. 2012-07-29 03:52:24 +02:00
f5bc081fda Send filenames with multipart/form-data file uploads. 2012-07-28 13:24:44 +02:00
1efea59a8d Fixed typos. 2012-07-28 06:09:25 +02:00
098e1d3100 Fixed multipart requests output; binary support.
* Bodies of multipart requests are correctly printed (closes #30).
* Binary requests and responses should always work (they are also suppressed
  for terminal output). So things like this work::

     http www.google.com/favicon.ico > favicon.ico
2012-07-28 05:50:12 +02:00
a8ddb8301d Default to https:// if invoked as `https'. 2012-07-27 18:08:33 +02:00
a770d79aef v0.2.7dev 2012-07-26 10:03:34 +02:00
b53d483163 v0.2.6 2012-07-26 09:58:31 +02:00
f45cc0eec0 Added docstrings, refactored input. 2012-07-26 07:23:00 +02:00
f26f2f1438 Mention necessary quoting with :=. #77 2012-07-26 03:24:58 +02:00
851412c698 Improved error messages. 2012-07-26 03:16:42 +02:00
26a76e8243 Clean-up 2012-07-26 00:50:39 +02:00
f5cfd0143b Ensure that full querystring is printent with -v.
The `key==value` parameters weren't included in the Request-Line URL.

Also added tests.
2012-07-25 14:32:57 +02:00
9391c89205 Fixed RST formatting. 2012-07-24 17:22:04 +02:00
76ebe7c6db Short option for --headers is now -h.
-t has been removed, for usage use --help
2012-07-24 17:17:26 +02:00
7af08b6faa Allow multiple fields with the same name.
Applies to form data and URL params:

    http -f url a=1 a=2
    http url a==1 a==2
2012-07-24 17:00:02 +02:00
9944def703 Switched to "==" a the separator for URL params.
Also refactored item escaping.
2012-07-24 14:56:53 +02:00
728a1a195b Updated changelog. 2012-07-24 01:17:07 +02:00
2646ebaaed Replaced --ignore-http-status with --check-status.
The default behaviour now is to exit with 0 on HTTP errors
unless --check-status is set.
2012-07-24 01:09:14 +02:00
fba3912f2e Fixed tests. 2012-07-23 19:49:38 +02:00
0572158ba1 Added exit codes for HTTP 3xx, 4xx, 5xx (3, 4, 5).
Also added `--ignore-http-status` to ensure 0 exit status.

HTTP 3xx result in 0 exit status when `--allow-redirects` is set.
2012-07-23 19:40:50 +02:00
0a673613ef Fixed colorama initialization (#36). 2012-07-21 15:08:28 +02:00
19f760450f Use local httpbin for all tests if available. 2012-07-21 14:37:05 +02:00
35da44309f Undebug 2012-07-21 03:26:48 +02:00
ced6e33230 Fixed tests. 2012-07-21 03:22:47 +02:00
87042f65c9 Added models.Environment().
Refactoring and general cleanup.
2012-07-21 03:14:01 +02:00
c271715a98 Updated flags in README. 2012-07-20 23:51:05 +02:00
57fc606f6b Changed default --print to "b" if stdout piped.
If the output is piped to another program or redirected to a file,
the new default behaviour is to only print the response body.
(It can still be overriden via the ``--print`` flag.)
2012-07-20 23:43:04 +02:00
7d82b853ae Updated installation instructions. 2012-07-20 22:09:53 +02:00
16f23d8147 Improved highlighting of HTTP headers.
Closes #60.
2012-07-20 21:58:41 +02:00
ab7915d9e0 Updated changelog; added stable version README link. 2012-07-19 13:31:02 +03:00
1d6fcfff73 Merge pull request #72 from jakebasile/issue-61
Added Query String Parameters (param=:value).
2012-07-19 03:12:32 -07:00
76a3125153 Updated documentation for query string params. 2012-07-18 21:16:33 -05:00
24d6331d15 Added a bit of testing for the new query string parameters. 2012-07-18 21:16:08 -05:00
06ea36aaa4 Added the ability to pass query string parameters. 2012-07-18 20:44:09 -05:00
c2d70e2bb1 Clean up. 2012-07-17 07:01:30 +02:00
40948dbd2e Updated changelog. 2012-07-17 04:20:37 +02:00
2dba176aa8 Added support for terminal colors under Windows.
Tested on Python 2.7 under Windows 7 with PowerShell and cmd.exe.

Closes #36
2012-07-17 04:06:13 +02:00
54e3e5bca4 README fixes. 2012-07-17 01:55:12 +02:00
533a662651 0.2.6dev 2012-07-17 01:39:30 +02:00
1ce02ebbd5 0.2.5 (bugfixes) 2012-07-17 01:39:02 +02:00
8a7f4c0d6e Fixed tests exist status. 2012-07-17 01:33:18 +02:00
f29c458611 Python 3 fixes. 2012-07-17 01:26:21 +02:00
2d7df0afb4 Fixed AttributeError in Content-Type vendor removal. 2012-07-17 01:11:43 +02:00
16a7d0a719 Fixed accidentally remove __licence__. 2012-07-17 01:11:01 +02:00
0cffda86f6 0.2.5dev 2012-07-17 00:47:42 +02:00
f42ee6da85 0.2.5dev 2012-07-17 00:45:20 +02:00
deeb7cbbac 0.2.4 (bad upload of 0.2.3 to pypi). 2012-07-17 00:44:25 +02:00
12f2fb4a92 Merge branch 'master' of github.com:jkbr/httpie 2012-07-17 00:38:41 +02:00
489bd64295 0.2.4dev 2012-07-17 00:37:53 +02:00
9b8cb42efd 0.2.3 2012-07-17 00:37:13 +02:00
2036337a53 Merge pull request #69 from jokull/master
Prettify vendor+json and vendor+xml Content-Type responses
2012-07-16 15:27:50 -07:00
5ca8bec9ff Add a note on pretty JSON and unicode to changelog
Closes #52
Closes #67
2012-07-17 00:22:39 +02:00
df79792fd9 Added test case to verify unicode output 2012-07-17 00:09:01 +02:00
5a82c79fdf Now non-ascii symbols displayed correctly in the output (not as escape sequences). 2012-07-17 00:08:52 +02:00
05b321d38f Better wording. 2012-07-17 00:06:13 +02:00
681b652bf9 Allow stdin data with password prompt; added tests
Closes #70
2012-07-16 23:41:27 +02:00
85b3a016eb Update README with new --auth behavior. 2012-07-16 04:50:25 -04:00
929ead437a Have --auth prompt for password if omitted. 2012-07-16 04:40:36 -04:00
36de166b28 Simplify vendor extension content-types since they are most likely lexable 2012-07-14 14:27:11 +00:00
7bc2de2f9d Merge pull request #68 from cemaleker/master
Added omitted query string data to request headers.
2012-07-13 17:53:11 -07:00
cb7ead04e2 Added omitted query string data to request headers. 2012-07-14 03:37:24 +03:00
cd2ca41f48 Merge pull request #65 from simono/patch-1
Update README.rst and add links to Ubuntu and Debian Packages.
2012-07-11 06:35:28 -07:00
c71de95505 Update README.rst and add links to Ubuntu and Debian Packages. 2012-07-11 16:32:00 +03:00
6ab03b21b4 Fixed Content-Type for requests with no data.
Closes #62.
2012-07-04 01:39:21 +02:00
50196be0f2 Added support for request payload from a filepath
Content-Type is detected from the filename.

Closes #57.
2012-06-29 00:45:31 +02:00
41d640920c Added more examples. 2012-06-25 14:50:49 +02:00
3179631603 0.2.3dev 2012-06-24 16:45:01 +02:00
2f7921091c 0.2.2 2012-06-24 16:43:03 +02:00
180313d80c Impreved tests. 2012-06-24 04:20:45 +02:00
926d3f5caf Tests, docs, clean-up.
Closes #54.
2012-06-24 03:45:21 +02:00
4613d947a8 Default to POST also when stdin redirected.
+clean up
2012-06-24 01:25:30 +02:00
5a47f00bac Replaced mock.Mock with argparse.Namespace to reduce deps. 2012-06-23 23:54:59 +02:00
0e1affbbc4 Issue #54 Method suggestion proposal 2012-06-17 22:15:07 +04:00
d920f20847 Issue #54 Method suggestion proposal 2012-06-17 22:11:26 +04:00
bca36f0464 Issue #54 Method suggestion proposal 2012-06-17 21:46:56 +04:00
78fff98712 Issue #54 Method suggestion proposal 2012-06-16 20:08:31 +04:00
e06c448a75 README improvements. 2012-06-15 17:32:45 +02:00
9cdbd6b0ec Added a Contribute section to README. 2012-06-15 17:13:40 +02:00
cbc6d02127 Fixed --verbose --form.
Closes #53
2012-06-15 16:47:55 +02:00
284a75fa2f Merge pull request #51 from msabramo/testing
Added support for tox (http://tox.testrun.org/)
2012-06-13 07:48:09 -07:00
b3ea273a21 Add "pypy" to .travis.yml 2012-06-13 07:36:51 -07:00
0d129d5f69 Add tox.ini for tox (http://tox.testrun.org/) 2012-06-13 07:18:12 -07:00
1388206f1a Fix path to tests.py in setup.py to make python setup.py test work 2012-06-13 07:18:11 -07:00
28dbe9f76c Bump version to 0.2.2dev. 2012-06-13 16:02:30 +02:00
82 changed files with 9059 additions and 1075 deletions

1
.coveragerc Normal file
View File

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

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_size = 2
[Makefile]
indent_style = tab
indent_size = 8

15
.gitignore vendored
View File

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

View File

@ -1,11 +1,94 @@
# https://travis-ci.org/jkbrzt/httpie
sudo: false
language: python
os:
- linux
env:
global:
- NEWEST_PYTHON=3.5
python:
- 2.6
- 2.7
- 3.1
- 3.2
script: python tests/tests.py
install:
- pip install requests pygments
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $TRAVIS_PYTHON_VERSION == '3.1' ]]; then pip install argparse; fi"
- pypy
- 3.4
- 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:
- |
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:
- |
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

40
AUTHORS.rst Normal file
View File

@ -0,0 +1,40 @@
==============
HTTPie authors
==============
* `Jakub Roztocil <https://github.com/jkbrzt>`_
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)
* `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_
* `Vladimir Berkutov <https://github.com/dair-targ>`_
* `Jakob Kramer <https://github.com/gandaro>`_
* `Chris Faulkner <https://github.com/faulkner>`_
* `Alen Mujezinovic <https://github.com/flashingpumpkin>`_
* `Praful Mathur <https://github.com/tictactix>`_
* `Marc Abramowitz <https://github.com/msabramo>`_
* `Ismail Badawi <https://github.com/isbadawi>`_
* `Laurent Bachelier <https://github.com/laurentb>`_
* `Isman Firmansyah <https://github.com/iromli>`_
* `Simon Olofsson <https://github.com/simono>`_
* `Churkin Oleg <https://github.com/Bahus>`_
* `Jökull Sólberg Auðunsson <https://github.com/jokull>`_
* `Matthew M. Boedicker <https://github.com/mmb>`_
* `marblar <https://github.com/marblar>`_
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
* `Davey Shafik <https://github.com/dshafik>`_
* `cido <https://github.com/cido>`_
* `Justin Bonnar <https://github.com/jargonjustin>`_
* `Nathan LaFreniere <https://github.com/nlf>`_
* `Matthias Lehmann <https://github.com/matleh>`_
* `Dennis Brakhane <https://github.com/brakhane>`_
* `Matt Layman <https://github.com/mblayman>`_
* `Edward Yang <https://github.com/honorabrutroll>`_

325
CHANGELOG.rst Normal file
View File

@ -0,0 +1,325 @@
==========
Change Log
==========
This document records all notable changes to `HTTPie <http://httpie.org>`_.
This project adheres to `Semantic Versioning <http://semver.org/>`_.
`1.0.0-dev`_ (unreleased)
-------------------------
`0.9.8`_ (2016-12-08)
---------------------
* Extended auth plugin API.
* Added exit status code ``7`` for plugin errors.
* Added support for ``curses``-less Python installations.
* Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required.
* Improved ``CTRL-C`` interrupt handling.
* Added the standard exit status code ``130`` for keyboard interrupts.
`0.9.6`_ (2016-08-13)
---------------------
* Added Python 3 as a dependency for Homebrew installations
to ensure some of the newer HTTP features work out of the box
for macOS users (starting with HTTPie 0.9.4.).
* Added the ability to unset a request header with ``Header:``, and send an
empty value with ``Header;``.
* Added ``--default-scheme <URL_SCHEME>`` to enable things like
``$ alias https='http --default-scheme=https``.
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
* Added fish shell completion (located in ``extras/httpie-completion.fish``
in the Github repo).
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
``pip install requests[socks]``.
* Changed the default JSON ``Accept`` header from ``application/json``
to ``application/json, */*``.
* Changed the pre-processing of request HTTP headers so that any leading
and trailing whitespace is removed.
`0.9.4`_ (2016-07-01)
---------------------
* 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``
* Added basic Bash autocomplete support (need to be installed manually)
* Added request details to connection error messages
* Fixed ``'requests.packages.urllib3' has no attribute 'disable_warnings'``
errors that occurred in some installations
* Fixed colors and formatting on Windows
* Fixed ``--auth`` prompt on Windows
`0.9.2`_ (2015-02-24)
---------------------
* Fixed compatibility with Requests 2.5.1
* Changed the default JSON ``Content-Type`` to ``application/json`` as UTF-8
is the default JSON encoding
`0.9.1`_ (2015-02-07)
---------------------
* Added support for Requests transport adapter plugins
(see `httpie-unixsocket <https://github.com/msabramo/httpie-unixsocket>`_
and `httpie-http2 <https://github.com/jkbrzt/httpie-http2>`_)
`0.9.0`_ (2015-01-31)
---------------------
* Added ``--cert`` and ``--cert-key`` parameters to specify a client side
certificate and private key for SSL
* Improved unicode support
* Improved terminal color depth detection via ``curses``
* To make it easier to deal with Windows paths in request items, ``\``
now only escapes special characters (the ones that are used as key-value
separators by HTTPie)
* Switched from ``unittest`` to ``pytest``
* Added Python `wheel` support
* Various test suite improvements
* Added ``CONTRIBUTING``
* Fixed ``User-Agent`` overwriting when used within a session
* Fixed handling of empty passwords in URL credentials
* Fixed multiple file uploads with the same form field name
* Fixed ``--output=/dev/null`` on Linux
* Miscellaneous bugfixes
`0.8.0`_ (2014-01-25)
---------------------
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
the contents of text and JSON files into request data
* Added curl-style shorthand for localhost
* Fixed request ``Host`` header value output so that it doesn't contain
credentials, if included in the URL
`0.7.1`_ (2013-09-24)
---------------------
* Added ``--ignore-stdin``
* Added support for auth plugins
* Improved ``--help`` output
* Improved ``Content-Disposition`` parsing for ``--download`` mode
* Update to Requests 2.0.0
`0.6.0`_ (2013-06-03)
---------------------
* XML data is now formatted
* ``--session`` and ``--session-read-only`` now also accept paths to
session files (eg. ``http --session=/tmp/session.json example.org``)
`0.5.1`_ (2013-05-13)
---------------------
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
anymore as they are request-specific
`0.5.0`_ (2013-04-27)
---------------------
* Added a download mode via ``--download``
* Fixes miscellaneous bugs
`0.4.1`_ (2013-02-26)
---------------------
* Fixed ``setup.py``
`0.4.0`_ (2013-02-22)
---------------------
* Added Python 3.3 compatibility
* Added Requests >= v1.0.4 compatibility
* Added support for credentials in URL
* Added ``--no-option`` for every ``--option`` to be config-friendly
* Mutually exclusive arguments can be specified multiple times. The
last value is used
`0.3.0`_ (2012-09-21)
---------------------
* Allow output redirection on Windows
* Added configuration file
* Added persistent session support
* Renamed ``--allow-redirects`` to ``--follow``
* Improved the usability of ``http --help``
* Fixed installation on Windows with Python 3
* Fixed colorized output on Windows with Python 3
* CRLF HTTP header field separation in the output
* Added exit status code ``2`` for timed-out requests
* Added the option to separate colorizing and formatting
(``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``)
``--ugly`` has bee removed in favor of ``--pretty=none``
`0.2.7`_ (2012-08-07)
---------------------
* Added compatibility with Requests 0.13.6
* Added streamed terminal output. ``--stream, -S`` can be used to enable
streaming also with ``--pretty`` and to ensure a more frequent output
flushing
* Added support for efficient large file downloads
* Sort headers by name (unless ``--pretty=none``)
* Response body is fetched only when needed (e.g., not with ``--headers``)
* Improved content type matching
* Updated Solarized color scheme
* Windows: Added ``--output FILE`` to store output into a file
(piping results in corrupted data on Windows)
* Proper handling of binary requests and responses
* Fixed printing of ``multipart/form-data`` requests
* Renamed ``--traceback`` to ``--debug``
`0.2.6`_ (2012-07-26)
---------------------
* The short option for ``--headers`` is now ``-h`` (``-t`` has been
removed, for usage use ``--help``)
* Form data and URL parameters can have multiple fields with the same name
(e.g.,``http -f url a=1 a=2``)
* Added ``--check-status`` to exit with an error on HTTP 3xx, 4xx and
5xx (3, 4, and 5, respectively)
* If the output is piped to another program or redirected to a file,
the default behaviour is to only print the response body
(It can still be overwritten via the ``--print`` flag.)
* Improved highlighting of HTTP headers
* Added query string parameters (``param==value``)
* Added support for terminal colors under Windows
`0.2.5`_ (2012-07-17)
---------------------
* Unicode characters in prettified JSON now don't get escaped for
improved readability
* --auth now prompts for a password if only a username provided
* Added support for request payloads from a file path with automatic
``Content-Type`` (``http URL @/path``)
* Fixed missing query string when displaying the request headers via
``--verbose``
* Fixed Content-Type for requests with no data
`0.2.2`_ (2012-06-24)
---------------------
* The ``METHOD`` positional argument can now be omitted (defaults to
``GET``, or to ``POST`` with data)
* Fixed --verbose --form
* Added support for Tox
`0.2.1`_ (2012-06-13)
---------------------
* Added compatibility with ``requests-0.12.1``
* Dropped custom JSON and HTTP lexers in favor of the ones newly included
in ``pygments-1.5``
`0.2.0`_ (2012-04-25)
---------------------
* Added Python 3 support
* Added the ability to print the HTTP request as well as the response
(see ``--print`` and ``--verbose``)
* Added support for Digest authentication
* Added file upload support
(``http -f POST file_field_name@/path/to/file``)
* Improved syntax highlighting for JSON
* Added support for field name escaping
* Many bug fixes
`0.1.6`_ (2012-03-04)
---------------------
* Fixed ``setup.py``
`0.1.5`_ (2012-03-04)
---------------------
* Many improvements and bug fixes
`0.1.4`_ (2012-02-28)
---------------------
* Many improvements and bug fixes
`0.1`_ (2012-02-25)
-------------------
* Initial public release
.. _`0.1`: https://github.com/jkbrzt/httpie/commit/b966efa
.. _0.1.4: https://github.com/jkbrzt/httpie/compare/b966efa...0.1.4
.. _0.1.5: https://github.com/jkbrzt/httpie/compare/0.1.4...0.1.5
.. _0.1.6: https://github.com/jkbrzt/httpie/compare/0.1.5...0.1.6
.. _0.2.0: https://github.com/jkbrzt/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jkbrzt/httpie/compare/0.2.0...0.2.1
.. _0.2.2: https://github.com/jkbrzt/httpie/compare/0.2.1...0.2.2
.. _0.2.5: https://github.com/jkbrzt/httpie/compare/0.2.2...0.2.5
.. _0.2.6: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jkbrzt/httpie/compare/0.2.5...0.2.7
.. _0.3.0: https://github.com/jkbrzt/httpie/compare/0.2.7...0.3.0
.. _0.4.0: https://github.com/jkbrzt/httpie/compare/0.3.0...0.4.0
.. _0.4.1: https://github.com/jkbrzt/httpie/compare/0.4.0...0.4.1
.. _0.5.0: https://github.com/jkbrzt/httpie/compare/0.4.1...0.5.0
.. _0.5.1: https://github.com/jkbrzt/httpie/compare/0.5.0...0.5.1
.. _0.6.0: https://github.com/jkbrzt/httpie/compare/0.5.1...0.6.0
.. _0.7.1: https://github.com/jkbrzt/httpie/compare/0.6.0...0.7.1
.. _0.8.0: https://github.com/jkbrzt/httpie/compare/0.7.1...0.8.0
.. _0.9.0: https://github.com/jkbrzt/httpie/compare/0.8.0...0.9.0
.. _0.9.1: https://github.com/jkbrzt/httpie/compare/0.9.0...0.9.1
.. _0.9.2: https://github.com/jkbrzt/httpie/compare/0.9.1...0.9.2
.. _0.9.3: https://github.com/jkbrzt/httpie/compare/0.9.2...0.9.3
.. _0.9.4: https://github.com/jkbrzt/httpie/compare/0.9.3...0.9.4
.. _0.9.6: https://github.com/jkbrzt/httpie/compare/0.9.4...0.9.6
.. _0.9.8: https://github.com/jkbrzt/httpie/compare/0.9.6...0.9.8
.. _1.0.0-dev: https://github.com/jkbrzt/httpie/compare/0.9.8...master

116
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,116 @@
######################
Contributing to HTTPie
######################
Bug reports and code and documentation patches are welcome. You can
help this project also by using the development version of HTTPie
and by reporting any bugs you might encounter.
1. Reporting bugs
=================
**It's important that you provide the full command argument list
as well as the output of the failing command.**
Use the ``--debug`` flag and copy&paste both the command and its output
to your bug report, e.g.:
.. code-block:: bash
$ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR]
[COMPLETE OUTPUT]
2. Contributing Code and Docs
=============================
Before working on a new feature or a bug, please browse `existing issues`_
to see whether it has been previously discussed. If the change in question
is a bigger one, it's always good to discuss before your starting working on
it.
Creating Development Environment
--------------------------------
Go to https://github.com/jkbrzt/httpie and fork the project repository.
.. code-block:: bash
git clone https://github.com/<YOU>/httpie
cd httpie
git checkout -b my_topical_branch
# (Recommended: create a new virtualenv)
# Install dev. requirements and also HTTPie (in editable mode
# so that the `http' command will point to your working copy):
make
Making Changes
--------------
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8).
Testing
-------
Before opening a pull requests, please make sure the `test suite`_ passes
in all of the `supported Python environments`_. You should also add tests
for any new features and bug fixes.
HTTPie uses `pytest`_ and `Tox`_ for testing.
Running all tests:
******************
.. code-block:: bash
# Run all tests on the current Python interpreter
make test
# Run all tests on the current Python with coverage
make test-cover
# Run all tests in all of the supported and available Pythons via Tox
make test-tox
# Run all tests for code as well as packaging, etc.
make test-all
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::TestMultipartFormDataFileUpload --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
-----
See `Makefile`_ for additional development utilities.
Don't forget to add yourself to `AUTHORS`_!
.. _Tox: http://tox.testrun.org
.. _supported Python environments: https://github.com/jkbrzt/httpie/blob/master/tox.ini
.. _existing issues: https://github.com/jkbrzt/httpie/issues?state=open
.. _AUTHORS: https://github.com/jkbrzt/httpie/blob/master/AUTHORS.rst
.. _Makefile: https://github.com/jkbrzt/httpie/blob/master/Makefile
.. _pytest: http://pytest.org/
.. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/
.. _test suite: https://github.com/jkbrzt/httpie/tree/master/tests

View File

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

View File

@ -1 +1,4 @@
include README.rst LICENSE
include LICENSE
include README.rst
include CHANGELOG.rst
include AUTHORS.rst

100
Makefile Normal file
View File

@ -0,0 +1,100 @@
#
# See ./CONTRIBUTING.rst
#
VERSION=$(shell grep __version__ httpie/__init__.py)
REQUIREMENTS="requirements-dev.txt"
TAG="\n\n\033[0;32m\#\#\# "
END=" \#\#\# \033[0m\n"
all: test
init: uninstall-httpie
@echo $(TAG)Installing dev requirements$(END)
pip install --upgrade -r $(REQUIREMENTS)
@echo $(TAG)Installing HTTPie$(END)
pip install --upgrade --editable .
@echo
test: init
@echo $(TAG)Running tests on the current Python interpreter with coverage $(END)
py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests
@echo
test-tox: init
@echo $(TAG)Running tests on all Pythons via Tox$(END)
tox
@echo
test-dist: test-sdist test-bdist-wheel
@echo
test-sdist: clean uninstall-httpie
@echo $(TAG)Testing sdist build an installation$(END)
python setup.py sdist
pip install --force-reinstall --upgrade dist/*.gz
which http
@echo
test-bdist-wheel: clean uninstall-httpie
@echo $(TAG)Testing wheel build an installation$(END)
python setup.py bdist_wheel
pip install --force-reinstall --upgrade dist/*.whl
which http
@echo
# This tests everything, even this Makefile.
test-all: uninstall-all clean init test test-tox test-dist
publish: test-all publish-no-test
publish-no-test:
@echo $(TAG)Testing wheel build an installation$(END)
@echo "$(VERSION)"
@echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok
python setup.py register
python setup.py sdist upload
python setup.py bdist_wheel upload
@echo
clean:
@echo $(TAG)Cleaning up$(END)
rm -rf .tox *.egg dist build .coverage
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
@echo
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

1738
README.rst

File diff suppressed because it is too large Load Diff

23
appveyor.yml Normal file
View File

@ -0,0 +1,23 @@
# https://ci.appveyor.com/project/jkbrzt/httpie
build: false
environment:
matrix:
- PYTHON: "C:/Python27"
# Python 3.4 has outdated pip
# - PYTHON: "C:/Python34"
- PYTHON: "C:/Python35"
init:
- "ECHO %PYTHON%"
- ps: "ls C:/Python*"
install:
# FIXME: updating pip fails with PermissionError
# - "%PYTHON%/Scripts/pip.exe install -U pip setuptools"
- "%PYTHON%/Scripts/pip.exe install -e ."
test_script:
- "%PYTHON%/Scripts/pip.exe --version"
- "%PYTHON%/Scripts/http.exe --debug"
- "%PYTHON%/python.exe setup.py test"

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,23 @@
#!/usr/bin/env bash
_http_complete() {
local cur_word=${COMP_WORDS[COMP_CWORD]}
local prev_word=${COMP_WORDS[COMP_CWORD - 1]}
if [[ "$cur_word" == -* ]]; then
_http_complete_options "$cur_word"
fi
}
complete -o default -F _http_complete http
_http_complete_options() {
local cur_word=$1
local options="-j --json -f --form --pretty -s --style -p --print
-v --verbose -h --headers -b --body -S --stream -o --output -d --download
-c --continue --session --session-read-only -a --auth --auth-type --proxy
--follow --verify --cert --cert-key --timeout --check-status --ignore-stdin
--help --version --traceback --debug"
COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) )
}

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'

47
extras/httpie.rb Normal file
View File

@ -0,0 +1,47 @@
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
# Only useful for testing until it gets accepted by homebrew maintainers.
#
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
#
class Httpie < Formula
desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.org/"
url "https://pypi.python.org/packages/10/cf/da63860ef92f9c90a5bd684d5f162067b26ef113b1c4afb9979c2f5c5a7a/httpie-0.9.7.tar.gz"
sha256 "6427c198c80b04e84963890261f29f1e3452b2b4b81e87a403bf22996754e6ec"
head "https://github.com/jkbrzt/httpie.git"
depends_on :python3
resource "requests" do
url "https://pypi.python.org/packages/d9/03/155b3e67fe35fe5b6f4227a8d9e96a14fda828b18199800d161bcefc1359/requests-2.12.3.tar.gz"
sha256 "de5d266953875e9647e37ef7bfe6ef1a46ff8ddfe61b5b3652edf7ea717ee2b2"
end
resource "pygments" do
url "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz"
sha256 "88e4c8a91b2af5962bfa5ea2447ec6dd357018e86e94c7d14bd8cacbc5b55d81"
end
def install
pyver = Language::Python.major_minor_version "python3"
ENV.prepend_create_path "PYTHONPATH", libexec/"vendor/lib/python#{pyver}/site-packages"
%w[pygments requests].each do |r|
resource(r).stage do
system "python3", *Language::Python.setup_install_args(libexec/"vendor")
end
end
ENV.prepend_create_path "PYTHONPATH", libexec/"lib/python#{pyver}/site-packages"
system "python3", *Language::Python.setup_install_args(libexec)
bin.install Dir["#{libexec}/bin/*"]
bin.env_script_all_files(libexec/"bin", :PYTHONPATH => ENV["PYTHONPATH"])
end
test do
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb"
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -1,7 +1,32 @@
"""
HTTPie - cURL for humans.
HTTPie - a CLI, cURL-like tool for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.2.1'
__version__ = '0.9.8'
__licence__ = 'BSD'
class ExitStatus:
"""Exit status code constants."""
OK = 0
ERROR = 1
PLUGIN_ERROR = 7
# 128+2 SIGINT <http://www.tldp.org/LDP/abs/html/exitcodes.html>
ERROR_CTRL_C = 130
ERROR_TIMEOUT = 2
ERROR_TOO_MANY_REDIRECTS = 6
# Used only when requested with --check-status:
ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5
EXIT_STATUS_LABELS = dict(
(value, key)
for key, value in ExitStatus.__dict__.items()
if key.isupper()
)

View File

@ -1,125 +1,17 @@
#!/usr/bin/env python
"""The main entry point. Invoke as `http' or `python -m httpie'.
"""
import sys
import json
import requests
from requests.compat import str
from . import httpmessage
from . import cliparse
from . import cli
from . import pretty
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; charset=utf-8'
def _get_response(parser, args, stdin, stdin_isatty):
if not stdin_isatty:
if args.data:
parser.error('Request body (stdin) and request '
'data (key=value) cannot be mixed.')
args.data = stdin.read()
if args.json or (not args.form and args.data):
# JSON
if not args.files and (
'Content-Type' not in args.headers
and (args.data or args.json)):
args.headers['Content-Type'] = TYPE_JSON
if stdin_isatty:
# Serialize the parsed data.
args.data = json.dumps(args.data)
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
elif not args.files and 'Content-Type' not in args.headers:
# Form
args.headers['Content-Type'] = TYPE_FORM
# Fire the request.
def main():
try:
credentials = None
if args.auth:
auth_type = (requests.auth.HTTPDigestAuth
if args.auth_type == 'digest'
else requests.auth.HTTPBasicAuth)
credentials = auth_type(args.auth.key, args.auth.value)
return requests.request(
method=args.method.lower(),
url=args.url if '://' in args.url else 'http://%s' % args.url,
headers=args.headers,
data=args.data,
verify={'yes': True, 'no': False}.get(args.verify, args.verify),
timeout=args.timeout,
auth=credentials,
proxies=dict((p.key, p.value) for p in args.proxy),
files=args.files,
allow_redirects=args.allow_redirects,
)
except (KeyboardInterrupt, SystemExit):
sys.stderr.write('\n')
sys.exit(1)
except Exception as e:
if args.traceback:
raise
sys.stderr.write(str(e.message) + '\n')
sys.exit(1)
def _get_output(args, stdout_isatty, response):
do_prettify = (args.prettify is True or
(args.prettify == cliparse.PRETTIFY_STDOUT_TTY_ONLY
and stdout_isatty))
do_output_request = (cliparse.OUT_REQ_HEADERS in args.output_options
or cliparse.OUT_REQ_BODY in args.output_options)
do_output_response = (cliparse.OUT_RESP_HEADERS in args.output_options
or cliparse.OUT_RESP_BODY in args.output_options)
prettifier = pretty.PrettyHttp(args.style) if do_prettify else None
output = []
if do_output_request:
output.append(httpmessage.format(
message=httpmessage.from_request(response.request),
prettifier=prettifier,
with_headers=cliparse.OUT_REQ_HEADERS in args.output_options,
with_body=cliparse.OUT_REQ_BODY in args.output_options
))
output.append('\n')
if do_output_response:
output.append('\n')
if do_output_response:
output.append(httpmessage.format(
message=httpmessage.from_response(response),
prettifier=prettifier,
with_headers=cliparse.OUT_RESP_HEADERS in args.output_options,
with_body=cliparse.OUT_RESP_BODY in args.output_options
))
output.append('\n')
return ''.join(output)
def main(args=None,
stdin=sys.stdin, stdin_isatty=sys.stdin.isatty(),
stdout=sys.stdout, stdout_isatty=sys.stdout.isatty()):
parser = cli.parser
args = parser.parse_args(args if args is not None else sys.argv[1:])
response = _get_response(parser, args, stdin, stdin_isatty)
output = _get_output(args, stdout_isatty, response)
output_bytes = output.encode('utf8')
f = (stdout.buffer if hasattr(stdout, 'buffer') else stdout)
f.write(output_bytes)
from .core import main
sys.exit(main())
except KeyboardInterrupt:
from . import ExitStatus
sys.exit(ExitStatus.ERROR_CTRL_C)
if __name__ == '__main__':

View File

@ -1,203 +1,648 @@
"""
CLI definition.
"""CLI arguments definition.
NOTE: the CLI interface may change before reaching v1.0.
"""
from . import pretty
from . import __doc__
from . import __version__
from . import cliparse
# noinspection PyCompatibility
from argparse import (
RawDescriptionHelpFormatter, FileType,
OPTIONAL, ZERO_OR_MORE, SUPPRESS
)
from textwrap import dedent, wrap
from httpie import __doc__, __version__
from httpie.input import (
HTTPieArgumentParser, KeyValueArgType,
SEP_PROXY, SEP_GROUP_ALL_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
readable_file_arg, SSL_VERSION_ARG_MAPPING
)
from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE
from httpie.plugins import plugin_manager
from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.sessions import DEFAULT_SESSIONS_DIR
def _(text):
"""Normalize white space."""
return ' '.join(text.strip().split())
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
"""A nicer help formatter.
Help for arguments can be indented and contain new lines.
It will be de-dented and arguments in the help
will be separated by a blank line for better readability.
desc = '%s <http://httpie.org>'
parser = cliparse.HTTPieArgumentParser(description=desc % __doc__.strip(),)
parser.add_argument('--version', action='version', version=__version__)
"""
def __init__(self, max_help_position=6, *args, **kwargs):
# A smaller indent for args help.
kwargs['max_help_position'] = max_help_position
super(HTTPieHelpFormatter, self).__init__(*args, **kwargs)
def _split_lines(self, text, width):
text = dedent(text).strip() + '\n\n'
return text.splitlines()
parser = HTTPieArgumentParser(
formatter_class=HTTPieHelpFormatter,
description='%s <http://httpie.org>' % __doc__.strip(),
epilog=dedent("""
For every --OPTION there is also a --no-OPTION that reverts OPTION
to its default value.
Suggestions and bug reports are greatly appreciated:
https://github.com/jkbrzt/httpie/issues
"""),
)
#######################################################################
# Positional arguments.
#######################################################################
positional = parser.add_argument_group(
title='Positional Arguments',
description=dedent("""
These arguments come after any flags and in the order they are listed here.
Only URL is required.
""")
)
positional.add_argument(
'method',
metavar='METHOD',
nargs=OPTIONAL,
default=None,
help="""
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
This argument can be omitted in which case HTTPie will use POST if there
is some data to be sent, otherwise GET:
$ http example.org # => GET
$ http example.org hello=world # => POST
"""
)
positional.add_argument(
'url',
metavar='URL',
help="""
The scheme defaults to 'http://' if the URL does not include one.
(You can override this with: --default-scheme=https)
You can also use a shorthand for localhost
$ http :3000 # => http://localhost:3000
$ http :/foo # => http://localhost/foo
"""
)
positional.add_argument(
'items',
metavar='REQUEST_ITEM',
nargs=ZERO_OR_MORE,
default=None,
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
help=r"""
Optional key-value pairs to be included in the request. The separator used
determines the type:
':' HTTP headers:
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
'==' URL parameters to be appended to the request URI:
search==httpie
'=' Data fields to be serialized into a JSON object (with --json, -j)
or form data (with --form, -f):
name=HTTPie language=Python description='CLI HTTP client'
':=' Non-string JSON data fields (only with --json, -j):
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
'@' Form file fields (only with --form, -f):
cs@~/Documents/CV.pdf
'=@' A data field like '=', but takes a file path and embeds its content:
essay=@Documents/essay.txt
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
package:=@./package.json
You can use a backslash to escape a colliding separator in the field name:
field-name-with\:colon=value
"""
)
#######################################################################
# Content type.
#############################################
#######################################################################
group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument(
'--json', '-j', action='store_true',
help=_('''
(default) Data items are serialized as a JSON object.
The Content-Type and Accept headers
are set to application/json (if not set via the command line).
''')
content_type = parser.add_argument_group(
title='Predefined Content Types',
description=None
)
group_type.add_argument(
'--form', '-f', action='store_true',
help=_('''
Data items are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded (if not specifid).
The presence of any file fields results into a multipart/form-data request.
''')
content_type.add_argument(
'--json', '-j',
action='store_true',
help="""
(default) Data items from the command line are serialized as a JSON object.
The Content-Type and Accept headers are set to application/json
(if not specified).
"""
)
content_type.add_argument(
'--form', '-f',
action='store_true',
help="""
Data items from the command line are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded (if not
specified). The presence of any file fields results in a
multipart/form-data request.
"""
)
# Output options.
#############################################
#######################################################################
# Output processing
#######################################################################
parser.add_argument(
'--traceback', action='store_true', default=False,
help=_('''
Print exception traceback should one occur.
''')
output_processing = parser.add_argument_group(title='Output Processing')
output_processing.add_argument(
'--pretty',
dest='prettify',
default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()),
help="""
Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors
and formatting (default for terminal output), "colors", or "format".
"""
)
output_processing.add_argument(
'--style', '-s',
dest='style',
metavar='STYLE',
default=DEFAULT_STYLE,
choices=AVAILABLE_STYLES,
help="""
Output coloring style (default is "{default}"). One of:
{available}
For this option to work properly, please make sure that the $TERM
environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
""".format(
default=DEFAULT_STYLE,
available='\n'.join(
'{0}{1}'.format(8 * ' ', line.strip())
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
).rstrip(),
)
)
prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument(
'--pretty', dest='prettify', action='store_true',
default=cliparse.PRETTIFY_STDOUT_TTY_ONLY,
help=_('''
If stdout is a terminal, the response is prettified
by default (colorized and indented if it is JSON).
This flag ensures prettifying even when stdout is redirected.
''')
)
prettify.add_argument(
'--ugly', '-u', dest='prettify', action='store_false',
help=_('''
Do not prettify the response.
''')
)
output_options = parser.add_mutually_exclusive_group(required=False)
output_options.add_argument('--print', '-p', dest='output_options',
default=cliparse.OUT_RESP_HEADERS + cliparse.OUT_RESP_BODY,
help=_('''
String specifying what should the output contain.
"{request_headers}" stands for the request headers and
"{request_body}" for the request body.
"{response_headers}" stands for the response headers and
"{response_body}" for response the body.
Defaults to "hb" which means that the whole response
(headers and body) is printed.
'''.format(
request_headers=cliparse.OUT_REQ_HEADERS,
request_body=cliparse.OUT_REQ_BODY,
response_headers=cliparse.OUT_RESP_HEADERS,
response_body=cliparse.OUT_RESP_BODY,
))
#######################################################################
# Output options
#######################################################################
output_options = parser.add_argument_group(title='Output Options')
output_options.add_argument(
'--print', '-p',
dest='output_options',
metavar='WHAT',
help="""
String specifying what the output should contain:
'{req_head}' request headers
'{req_body}' request body
'{res_head}' response headers
'{res_body}' response body
The default behaviour is '{default}' (i.e., the response headers and body
is printed), if standard output is not redirected. If the output is piped
to another program or to a file, then only the response body is printed
by default.
"""
.format(
req_head=OUT_REQ_HEAD,
req_body=OUT_REQ_BODY,
res_head=OUT_RESP_HEAD,
res_body=OUT_RESP_BODY,
default=OUTPUT_OPTIONS_DEFAULT,
)
)
output_options.add_argument(
'--verbose', '-v', dest='output_options',
action='store_const', const=''.join(cliparse.OUTPUT_OPTIONS),
help=_('''
Print the whole request as well as the response.
Shortcut for --print={0}.
'''.format(''.join(cliparse.OUTPUT_OPTIONS)))
'--headers', '-h',
dest='output_options',
action='store_const',
const=OUT_RESP_HEAD,
help="""
Print only the response headers. Shortcut for --print={0}.
"""
.format(OUT_RESP_HEAD)
)
output_options.add_argument(
'--headers', '-t', dest='output_options',
action='store_const', const=cliparse.OUT_RESP_HEADERS,
help=_('''
Print only the response headers.
Shortcut for --print={0}.
'''.format(cliparse.OUT_RESP_HEADERS))
)
output_options.add_argument(
'--body', '-b', dest='output_options',
action='store_const', const=cliparse.OUT_RESP_BODY,
help=_('''
Print only the response body.
Shortcut for --print={0}.
'''.format(cliparse.OUT_RESP_BODY))
'--body', '-b',
dest='output_options',
action='store_const',
const=OUT_RESP_BODY,
help="""
Print only the response body. Shortcut for --print={0}.
"""
.format(OUT_RESP_BODY)
)
parser.add_argument(
'--style', '-s', dest='style', default='solarized', metavar='STYLE',
choices=pretty.AVAILABLE_STYLES,
help=_('''
Output coloring style, one of %s. Defaults to solarized.
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
output_options.add_argument(
'--verbose', '-v',
dest='verbose',
action='store_true',
help="""
Verbose output. Print the whole request as well as the response. Also print
any intermediary requests/responses (such as redirects).
It's a shortcut for: --all --print={0}
"""
.format(''.join(OUTPUT_OPTIONS))
)
output_options.add_argument(
'--all',
default=False,
action='store_true',
help="""
By default, only the final request/response is shown. Use this flag to show
any intermediary requests/responses as well. Intermediary requests include
followed redirects (with --follow), the first unauthorized request when
Digest auth is used (--auth=digest), etc.
"""
)
output_options.add_argument(
'--history-print', '-P',
dest='output_options_history',
metavar='WHAT',
help="""
The same as --print, -p but applies only to intermediary requests/responses
(such as redirects) when their inclusion is enabled with --all. If this
options is not specified, then they are formatted the same way as the final
response.
"""
)
output_options.add_argument(
'--stream', '-S',
action='store_true',
default=False,
help="""
Always stream the output by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied),
HTTPie fetches the whole response before it outputs the processed data.
Set this option when you want to continuously display a prettified
long-lived response, such as one from the Twitter streaming API.
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
"""
)
output_options.add_argument(
'--output', '-o',
type=FileType('a+b'),
dest='output_file',
metavar='FILE',
help="""
Save output to FILE instead of stdout. If --download is also set, then only
the response body is saved to FILE. Other parts of the HTTP exchange are
printed to stderr.
"""
)
output_options.add_argument(
'--download', '-d',
action='store_true',
default=False,
help="""
Do not print the response body to stdout. Rather, download it and store it
in a file. The filename is guessed unless specified with --output
[filename]. This action is similar to the default behaviour of wget.
"""
)
output_options.add_argument(
'--continue', '-c',
dest='download_resume',
action='store_true',
default=False,
help="""
Resume an interrupted download. Note that the --output option needs to be
specified as well.
"""
)
#######################################################################
# Sessions
#######################################################################
sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False)
session_name_validator = SessionNameValidator(
'Session name contains invalid characters.'
)
sessions.add_argument(
'--session',
metavar='SESSION_NAME_OR_PATH',
type=session_name_validator,
help="""
Create, or reuse and update a session. Within a session, custom headers,
auth credential, as well as any cookies sent by the server persist between
requests.
Session files are stored in:
{session_dir}/<HOST>/<SESSION_NAME>.json.
"""
.format(session_dir=DEFAULT_SESSIONS_DIR)
)
sessions.add_argument(
'--session-read-only',
metavar='SESSION_NAME_OR_PATH',
type=session_name_validator,
help="""
Create or read a session without updating it form the request/response
exchange.
"""
)
#######################################################################
# Authentication
#######################################################################
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a', help='username:password',
type=cliparse.KeyValueType(cliparse.SEP_COMMON)
)
auth = parser.add_argument_group(title='Authentication')
auth.add_argument(
'--auth', '-a',
default=None,
metavar='USER[:PASS]',
help="""
If only the username is provided (-a username), HTTPie will prompt
for the password.
parser.add_argument(
'--auth-type', choices=['basic', 'digest'],
help=_('The authentication mechanism to be used. Defaults to "basic".')
)
parser.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".
''')
)
parser.add_argument(
'--proxy', default=[], action='append',
type=cliparse.KeyValueType(cliparse.SEP_COMMON),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
''')
)
parser.add_argument(
'--allow-redirects', default=False, action='store_true',
help=_('''
Set this flag if full redirects are allowed
(e.g. re-POST-ing of data at new ``Location``)
''')
)
parser.add_argument(
'--timeout', type=float,
help=_('''
Float describes the timeout of the request
(Use socket.setdefaulttimeout() as fallback).
''')
""",
)
# Positional arguments.
#############################################
class _AuthTypeLazyChoices(object):
# Needed for plugin testing
parser.add_argument(
'method', metavar='METHOD',
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
''')
def __contains__(self, item):
return item in plugin_manager.get_auth_plugin_mapping()
def __iter__(self):
return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys()))
_auth_plugins = plugin_manager.get_auth_plugins()
auth.add_argument(
'--auth-type', '-A',
choices=_AuthTypeLazyChoices(),
default=None,
help="""
The authentication mechanism to be used. Defaults to "{default}".
{types}
"""
.format(default=_auth_plugins[0].auth_type, types='\n '.join(
'"{type}": {name}{package}{description}'.format(
type=plugin.auth_type,
name=plugin.name,
package=(
'' if issubclass(plugin, BuiltinAuthPlugin)
else ' (provided by %s)' % plugin.package_name
),
description=(
'' if not plugin.description else
'\n ' + ('\n '.join(wrap(plugin.description)))
)
)
for plugin in _auth_plugins
)),
)
parser.add_argument(
'url', metavar='URL',
help=_('''
The protocol defaults to http:// if the
URL does not include one.
''')
#######################################################################
# Network
#######################################################################
network = parser.add_argument_group(title='Network')
network.add_argument(
'--proxy',
default=[],
action='append',
metavar='PROTOCOL:PROXY_URL',
type=KeyValueArgType(SEP_PROXY),
help="""
String mapping protocol to the URL of the proxy
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
different protocols.
"""
)
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
type=cliparse.KeyValueType(
cliparse.SEP_COMMON,
cliparse.SEP_DATA,
cliparse.SEP_DATA_RAW_JSON,
cliparse.SEP_FILES
),
help=_('''
A key-value pair whose type is defined by the separator used. It can be an
HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value)
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding separator in the field name.
''')
network.add_argument(
'--follow', '-F',
default=False,
action='store_true',
help="""
Follow 30x Location redirects.
"""
)
network.add_argument(
'--max-redirects',
type=int,
default=30,
help="""
By default, requests have a limit of 30 redirects (works with --follow).
"""
)
network.add_argument(
'--timeout',
type=float,
default=30,
metavar='SECONDS',
help="""
The connection timeout of the request in seconds. The default value is
30 seconds.
"""
)
network.add_argument(
'--check-status',
default=False,
action='store_true',
help="""
By default, HTTPie exits with 0 when no network or other fatal errors
occur. This flag instructs HTTPie to also check the HTTP status code and
exit with an error if the status indicates one.
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
status code, HTTPie exits with 4 or 5 respectively. If the response is a
3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected.
"""
)
#######################################################################
# SSL
#######################################################################
ssl = parser.add_argument_group(title='SSL')
ssl.add_argument(
'--verify',
default='yes',
help="""
Set to "no" 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 = parser.add_argument_group(title='Troubleshooting')
troubleshooting.add_argument(
'--ignore-stdin', '-I',
action='store_true',
default=False,
help="""
Do not attempt to read stdin.
"""
)
troubleshooting.add_argument(
'--help',
action='help',
default=SUPPRESS,
help="""
Show this help message and exit.
"""
)
troubleshooting.add_argument(
'--version',
action='version',
version=__version__,
help="""
Show version and exit.
"""
)
troubleshooting.add_argument(
'--traceback',
action='store_true',
default=False,
help="""
Prints the exception traceback should one occur.
"""
)
troubleshooting.add_argument(
'--default-scheme',
default="http",
help="""
The default scheme to use if not specified in the URL.
"""
)
troubleshooting.add_argument(
'--debug',
action='store_true',
default=False,
help="""
Prints the exception traceback should one occur, as well as other
information useful for debugging HTTPie itself and for reporting bugs.
"""
)

173
httpie/client.py Normal file
View File

@ -0,0 +1,173 @@
import json
import sys
import requests
from requests.adapters import HTTPAdapter
from requests.packages import urllib3
from httpie import sessions
from httpie import __version__
from httpie.compat import str
from httpie.input import SSL_VERSION_ARG_MAPPING
from httpie.plugins import plugin_manager
from httpie.utils import repr_dict_nice
try:
# https://urllib3.readthedocs.io/en/latest/security.html
urllib3.disable_warnings()
except AttributeError:
# In some rare cases, the user may have an old version of the requests
# or urllib3, and there is no method called "disable_warnings." In these
# cases, we don't need to call the method.
# They may get some noisy output but execution shouldn't die. Move on.
pass
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
JSON_CONTENT_TYPE = 'application/json'
JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE)
DEFAULT_UA = 'HTTPie/%s' % __version__
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.mount(
'https://',
HTTPieHTTPAdapter(ssl_version=ssl_version)
)
for cls in plugin_manager.get_transport_plugins():
transport_plugin = cls()
requests_session.mount(prefix=transport_plugin.prefix,
adapter=transport_plugin.get_adapter())
return requests_session
def get_response(args, config_dir):
"""Send the request and return a `request.Response`."""
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:
kwargs = get_requests_kwargs(args)
if args.debug:
dump_request(kwargs)
response = requests_session.request(**kwargs)
else:
response = sessions.get_response(
requests_session=requests_session,
args=args,
config_dir=config_dir,
session_name=args.session or args.session_read_only,
read_only=bool(args.session_read_only),
)
return response
def dump_request(kwargs):
sys.stderr.write('\n>>> requests.request(**%s)\n\n'
% repr_dict_nice(kwargs))
def finalize_headers(headers):
final_headers = {}
for name, value in headers.items():
if value is not None:
# >leading or trailing LWS MAY be removed without
# >changing the semantics of the field value"
# -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
# Also, requests raises `InvalidHeader` for leading spaces.
value = value.strip()
if isinstance(value, str):
# See: https://github.com/jkbrzt/httpie/issues/212
value = value.encode('utf8')
final_headers[name] = value
return final_headers
def get_default_headers(args):
default_headers = {
'User-Agent': DEFAULT_UA
}
auto_json = args.data and not args.form
if args.json or auto_json:
default_headers['Accept'] = JSON_ACCEPT
if args.json or (auto_json and args.data):
default_headers['Content-Type'] = JSON_CONTENT_TYPE
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
default_headers['Content-Type'] = FORM_CONTENT_TYPE
return default_headers
def get_requests_kwargs(args, base_headers=None):
"""
Translate our `args` into `requests.request` keyword arguments.
"""
# Serialize JSON data, if needed.
data = args.data
auto_json = data and not args.form
if (args.json or auto_json) and isinstance(data, dict):
if data:
data = json.dumps(data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
data = ''
# Finalize headers.
headers = get_default_headers(args)
if base_headers:
headers.update(base_headers)
headers.update(args.headers)
headers = finalize_headers(headers)
cert = None
if args.cert:
cert = args.cert
if args.cert_key:
cert = cert, args.cert_key
kwargs = {
'stream': True,
'method': args.method.lower(),
'url': args.url,
'headers': headers,
'data': data,
'verify': {
'yes': True,
'no': False
}.get(args.verify, args.verify),
'cert': cert,
'timeout': args.timeout,
'auth': args.auth,
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.follow,
'params': args.params,
}
return kwargs

View File

@ -1,169 +0,0 @@
"""
CLI argument parsing logic.
"""
import os
import re
import json
import argparse
from collections import namedtuple
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
from . import __version__
SEP_COMMON = ':'
SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
OUT_REQ_HEADERS = 'H'
OUT_REQ_BODY = 'B'
OUT_RESP_HEADERS = 'h'
OUT_RESP_BODY = 'b'
OUTPUT_OPTIONS = [OUT_REQ_HEADERS,
OUT_REQ_BODY,
OUT_RESP_HEADERS,
OUT_RESP_BODY]
PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class HTTPieArgumentParser(argparse.ArgumentParser):
def parse_args(self, args=None, namespace=None):
args = super(HTTPieArgumentParser, self).parse_args(args, namespace)
self._validate_output_options(args)
self._validate_auth_options(args)
self._parse_items(args)
return args
def _parse_items(self, args):
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = OrderedDict()
args.files = OrderedDict()
try:
parse_items(items=args.items, headers=args.headers,
data=args.data, files=args.files)
except ParseError as e:
if args.traceback:
raise
self.error(e.message)
if args.files and not args.form:
# We could just switch to --form automatically here,
# but I think it's better to make it explicit.
self.error(
' You need to set the --form / -f flag to'
' to issue a multipart request. File fields: %s'
% ','.join(args.files.keys()))
def _validate_output_options(self, args):
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
if unknown_output_options:
self.error('Unknown output options: %s' % ','.join(unknown_output_options))
def _validate_auth_options(self, args):
if args.auth_type and not args.auth:
self.error('--auth-type can only be used with --auth')
class ParseError(Exception):
pass
KeyValue = namedtuple('KeyValue', ['key', 'value', 'sep', 'orig'])
class KeyValueType(object):
"""A type used with `argparse`."""
def __init__(self, *separators):
self.separators = separators
self.escapes = ['\\\\' + sep for sep in separators]
def __call__(self, string):
found = {}
found_escapes = []
for esc in self.escapes:
found_escapes += [m.span() for m in re.finditer(esc, string)]
for sep in self.separators:
matches = re.finditer(sep, string)
for match in matches:
start, end = match.span()
inside_escape = False
for estart, eend in found_escapes:
if start >= estart and end <= eend:
inside_escape = True
break
if not inside_escape:
found[start] = sep
if not found:
raise argparse.ArgumentTypeError(
'"%s" is not a valid value' % string)
# split the string at the earliest non-escaped separator.
seploc = min(found.keys())
sep = found[seploc]
key = string[:seploc]
value = string[seploc + len(sep):]
# remove escape chars
for sepstr in self.separators:
key = key.replace('\\' + sepstr, sepstr)
value = value.replace('\\' + sepstr, sepstr)
return KeyValue(key=key, value=value, sep=sep, orig=string)
def parse_items(items, data=None, headers=None, files=None):
"""Parse `KeyValueType` `items` into `data`, `headers` and `files`."""
if headers is None:
headers = {}
if data is None:
data = {}
if files is None:
files = {}
for item in items:
value = item.value
key = item.key
if item.sep == SEP_HEADERS:
target = headers
elif item.sep == SEP_FILES:
try:
value = open(os.path.expanduser(item.value), 'r')
except IOError as e:
raise ParseError(
'Invalid argument %r. %s' % (item.orig, e))
if not key:
key = os.path.basename(value.name)
target = files
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
if item.sep == SEP_DATA_RAW_JSON:
try:
value = json.loads(item.value)
except ValueError:
raise ParseError('%s is not valid JSON' % item.orig)
target = data
else:
raise ParseError('%s is not valid item' % item.orig)
if key in target:
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
target[key] = value
return headers, data, files

178
httpie/compat.py Normal file
View File

@ -0,0 +1,178 @@
"""
Python 2.6, 2.7, and 3.x compatibility.
"""
import sys
is_py2 = sys.version_info[0] == 2
is_py26 = sys.version_info[:2] == (2, 6)
is_py27 = sys.version_info[:2] == (2, 7)
is_py3 = sys.version_info[0] == 3
is_pypy = 'pypy' in sys.version.lower()
is_windows = 'win32' in str(sys.platform).lower()
if is_py2:
# noinspection PyShadowingBuiltins
bytes = str
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
str = unicode
elif is_py3:
# noinspection PyShadowingBuiltins
str = str
# noinspection PyShadowingBuiltins
bytes = bytes
try: # pragma: no cover
# noinspection PyUnresolvedReferences,PyCompatibility
from urllib.parse import urlsplit
except ImportError: # pragma: no cover
# noinspection PyUnresolvedReferences,PyCompatibility
from urlparse import urlsplit
try: # pragma: no cover
# noinspection PyCompatibility
from urllib.request import urlopen
except ImportError: # pragma: no cover
# noinspection PyCompatibility,PyUnresolvedReferences
from urllib2 import urlopen
try: # pragma: no cover
from collections import OrderedDict
except ImportError: # pragma: no cover
# Python 2.6 OrderedDict class, needed for headers, parameters, etc .###
# <https://pypi.python.org/pypi/ordereddict/1.1>
# noinspection PyCompatibility,PyUnresolvedReferences
from UserDict import DictMixin
# noinspection PyShadowingBuiltins,PyCompatibility
class OrderedDict(dict, DictMixin):
# Copyright (c) 2009 Raymond Hettinger
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
# noinspection PyMissingConstructor
def __init__(self, *args, **kwds):
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d'
% len(args))
try:
self.__end
except AttributeError:
self.clear()
self.update(*args, **kwds)
def clear(self):
self.__end = end = []
# noinspection PyUnusedLocal
end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next]
dict.clear(self)
def __setitem__(self, key, value):
if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True):
if not self:
raise KeyError('dictionary is empty')
if last:
# noinspection PyUnresolvedReferences
key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __reduce__(self):
items = [[k, self[k]] for k in self]
tmp = self.__map, self.__end
del self.__map, self.__end
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return self.__class__, (items,), inst_dict
return self.__class__, (items,)
def keys(self):
return list(self)
setdefault = DictMixin.setdefault
update = DictMixin.update
pop = DictMixin.pop
values = DictMixin.values
items = DictMixin.items
iterkeys = DictMixin.iterkeys
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
# noinspection PyMethodOverriding
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other

112
httpie/config.py Normal file
View File

@ -0,0 +1,112 @@
import os
import json
import errno
from httpie import __version__
from httpie.compat import is_windows
DEFAULT_CONFIG_DIR = str(os.environ.get(
'HTTPIE_CONFIG_DIR',
os.path.expanduser('~/.httpie') if not is_windows else
os.path.expandvars(r'%APPDATA%\\httpie')
))
class BaseConfigDict(dict):
name = None
helpurl = None
about = None
def __getattr__(self, item):
return self[item]
def _get_path(self):
"""Return the config file path without side-effects."""
raise NotImplementedError()
@property
def path(self):
"""Return the config file path creating basedir, if needed."""
path = self._get_path()
try:
os.makedirs(os.path.dirname(path), mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return path
def is_new(self):
return not os.path.exists(self._get_path())
def load(self):
try:
with open(self.path, 'rt') as f:
try:
data = json.load(f)
except ValueError as e:
raise ValueError(
'Invalid %s JSON: %s [%s]' %
(type(self).__name__, str(e), self.path)
)
self.update(data)
except IOError as e:
if e.errno != errno.ENOENT:
raise
def save(self):
self['__meta__'] = {
'httpie': __version__
}
if self.helpurl:
self['__meta__']['help'] = self.helpurl
if self.about:
self['__meta__']['about'] = self.about
with open(self.path, 'w') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write('\n')
def delete(self):
try:
os.unlink(self.path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
class Config(BaseConfigDict):
name = 'config'
helpurl = 'https://httpie.org/docs#config'
about = 'HTTPie configuration file'
DEFAULTS = {
'default_options': []
}
def __init__(self, directory=DEFAULT_CONFIG_DIR):
super(Config, self).__init__()
self.update(self.DEFAULTS)
self.directory = directory
def load(self):
super(Config, self).load()
self._migrate_implicit_content_type()
def _get_path(self):
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()

99
httpie/context.py Normal file
View File

@ -0,0 +1,99 @@
import sys
try:
import curses
except ImportError:
curses = None # Compiled w/o curses
from httpie.compat import is_windows
from httpie.config import DEFAULT_CONFIG_DIR, Config
from httpie.utils import repr_dict_nice
class Environment(object):
"""
Information about the execution context
(standard streams, config directory, etc).
By default, it represents the actual environment.
All of the attributes can be overwritten though, which
is used by the test suite to simulate various scenarios.
"""
is_windows = is_windows
config_dir = DEFAULT_CONFIG_DIR
stdin = sys.stdin
stdin_isatty = stdin.isatty()
stdin_encoding = None
stdout = sys.stdout
stdout_isatty = stdout.isatty()
stdout_encoding = None
stderr = sys.stderr
stderr_isatty = stderr.isatty()
colors = 256
if not is_windows:
if curses:
try:
curses.setupterm()
colors = curses.tigetnum('colors')
except curses.error:
pass
else:
# noinspection PyUnresolvedReferences
import colorama.initialise
stdout = colorama.initialise.wrap_stream(
stdout, convert=None, strip=None,
autoreset=True, wrap=True
)
stderr = colorama.initialise.wrap_stream(
stderr, convert=None, strip=None,
autoreset=True, wrap=True
)
del colorama
def __init__(self, **kwargs):
"""
Use keyword arguments to overwrite
any of the class attributes for this instance.
"""
assert all(hasattr(type(self), attr) for attr in kwargs.keys())
self.__dict__.update(**kwargs)
# Keyword arguments > stream.encoding > default utf8
if self.stdin_encoding is None:
self.stdin_encoding = getattr(
self.stdin, 'encoding', None) or 'utf8'
if self.stdout_encoding is None:
actual_stdout = self.stdout
if is_windows:
# noinspection PyUnresolvedReferences
from colorama import AnsiToWin32
if isinstance(self.stdout, AnsiToWin32):
actual_stdout = self.stdout.wrapped
self.stdout_encoding = getattr(
actual_stdout, 'encoding', None) or 'utf8'
@property
def config(self):
if not hasattr(self, '_config'):
self._config = Config(directory=self.config_dir)
if self._config.is_new():
self._config.save()
else:
self._config.load()
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))

260
httpie/core.py Normal file
View File

@ -0,0 +1,260 @@
"""This module provides the main functionality of HTTPie.
Invocation flow:
1. Read, validate and process the input (args, `stdin`).
2. Create and send a request.
3. Stream, and possibly process and format, the parts
of the request-response exchange selected by output options.
4. Simultaneously write to `stdout`
5. Exit.
"""
import sys
import errno
import platform
import requests
from requests import __version__ as requests_version
from pygments import __version__ as pygments_version
from httpie import __version__ as httpie_version, ExitStatus
from httpie.compat import str, bytes, is_py3
from httpie.client import get_response
from httpie.downloads import Downloader
from httpie.context import Environment
from httpie.plugins import plugin_manager
from httpie.output.streams import (
build_output_stream,
write_stream,
write_stream_with_colors_win_py3
)
def get_exit_status(http_status, follow=False):
"""Translate HTTP status code to exit status code."""
if 300 <= http_status <= 399 and not follow:
# Redirect
return ExitStatus.ERROR_HTTP_3XX
elif 400 <= http_status <= 499:
# Client Error
return ExitStatus.ERROR_HTTP_4XX
elif 500 <= http_status <= 599:
# Server Error
return ExitStatus.ERROR_HTTP_5XX
else:
return ExitStatus.OK
def print_debug_info(env):
env.stderr.writelines([
'HTTPie %s\n' % httpie_version,
'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version,
'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):
"""
Convert all bytes ags to str
by decoding them using stdin encoding.
"""
return [
arg.decode(stdin_encoding)
if type(arg) == bytes else arg
for arg in args
]
def program(args, env, log_error):
"""
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.
"""
args = decode_args(args, env.stdin_encoding)
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
if env.config.default_options:
args = env.config.default_options + args
if custom_log_error:
log_error = custom_log_error
include_debug_info = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args
if include_debug_info:
print_debug_info(env)
if args == ['--debug']:
return ExitStatus.OK
exit_status = ExitStatus.OK
try:
parsed_args = parser.parse_args(args=args, env=env)
except KeyboardInterrupt:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.OK:
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_CTRL_C
except SystemExit as e:
if e.code != ExitStatus.OK:
env.stderr.write('\n')
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_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:
# TODO: Further distinction between expected and unexpected errors.
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg += ' while doing %s request to URL: %s' % (
request.method, request.url)
log_error('%s: %s', type(e).__name__, msg)
if include_traceback:
raise
exit_status = ExitStatus.ERROR
return exit_status

471
httpie/downloads.py Normal file
View File

@ -0,0 +1,471 @@
# coding=utf-8
"""
Download mode implementation.
"""
from __future__ import division
import os
import re
import sys
import errno
import mimetypes
import threading
from time import sleep, time
from mailbox import Message
from httpie.output.streams import RawStream
from httpie.models import HTTPResponse
from httpie.utils import humanize_bytes
from httpie.compat import urlsplit
PARTIAL_CONTENT = 206
CLEAR_LINE = '\r\033[K'
PROGRESS = (
'{percentage: 6.2f} %'
' {downloaded: >10}'
' {speed: >10}/s'
' {eta: >8} ETA'
)
PROGRESS_NO_CONTENT_LENGTH = '{downloaded: >10} {speed: >10}/s'
SUMMARY = 'Done. {downloaded} in {time:0.5f}s ({speed}/s)\n'
SPINNER = '|/-\\'
class ContentRangeError(ValueError):
pass
def parse_content_range(content_range, resumed_from):
"""
Parse and validate Content-Range header.
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html>
:param content_range: the value of a Content-Range response header
eg. "bytes 21010-47021/47022"
:param resumed_from: first byte pos. from the Range request header
:return: total size of the response body when fully downloaded.
"""
if content_range is None:
raise ContentRangeError('Missing Content-Range')
pattern = (
'^bytes (?P<first_byte_pos>\d+)-(?P<last_byte_pos>\d+)'
'/(\*|(?P<instance_length>\d+))$'
)
match = re.match(pattern, content_range)
if not match:
raise ContentRangeError(
'Invalid Content-Range format %r' % content_range)
content_range_dict = match.groupdict()
first_byte_pos = int(content_range_dict['first_byte_pos'])
last_byte_pos = int(content_range_dict['last_byte_pos'])
instance_length = (
int(content_range_dict['instance_length'])
if content_range_dict['instance_length']
else None
)
# "A byte-content-range-spec with a byte-range-resp-spec whose
# last- byte-pos value is less than its first-byte-pos value,
# or whose instance-length value is less than or equal to its
# last-byte-pos value, is invalid. The recipient of an invalid
# byte-content-range- spec MUST ignore it and any content
# transferred along with it."
if (first_byte_pos >= last_byte_pos or
(instance_length is not None and
instance_length <= last_byte_pos)):
raise ContentRangeError(
'Invalid Content-Range returned: %r' % content_range)
if (first_byte_pos != resumed_from or
(instance_length is not None and
last_byte_pos + 1 != instance_length)):
# Not what we asked for.
raise ContentRangeError(
'Unexpected Content-Range returned (%r)'
' for the requested Range ("bytes=%d-")'
% (content_range, resumed_from)
)
return last_byte_pos + 1
def filename_from_content_disposition(content_disposition):
"""
Extract and validate filename from a Content-Disposition header.
:param content_disposition: Content-Disposition value
:return: the filename if present and valid, otherwise `None`
"""
# attachment; filename=jkbrzt-httpie-0.4.1-20-g40bd8f6.tar.gz
msg = Message('Content-Disposition: %s' % content_disposition)
filename = msg.get_filename()
if filename:
# Basic sanitation.
filename = os.path.basename(filename).lstrip('.').strip()
if filename:
return filename
def filename_from_url(url, content_type):
fn = urlsplit(url).path.rstrip('/')
fn = os.path.basename(fn) if fn else 'index'
if '.' not in fn and content_type:
content_type = content_type.split(';')[0]
if content_type == 'text/plain':
# mimetypes returns '.ksh'
ext = '.txt'
else:
ext = mimetypes.guess_extension(content_type)
if ext == '.htm': # Python 3
ext = '.html'
if ext:
fn += ext
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):
attempt = 0
while True:
suffix = '-' + str(attempt) if attempt > 0 else ''
try_filename = trim_filename_if_needed(filename, extra=len(suffix))
try_filename += suffix
if not exists(try_filename):
return try_filename
attempt += 1
class Downloader(object):
def __init__(self, output_file=None,
resume=False, progress_file=sys.stderr):
"""
:param resume: Should the download resume if partial download
already exists.
:type resume: bool
:param output_file: The file to store response body in. If not
provided, it will be guessed from the response.
:param progress_file: Where to report download progress.
"""
self._output_file = output_file
self._resume = resume
self._resumed_from = 0
self.finished = False
self.status = Status()
self._progress_reporter = ProgressReporterThread(
status=self.status,
output=progress_file
)
def pre_request(self, request_headers):
"""Called just before the HTTP request is sent.
Might alter `request_headers`.
:type request_headers: dict
"""
# Ask the server not to encode the content so that we can resume, etc.
request_headers['Accept-Encoding'] = 'identity'
if self._resume:
bytes_have = os.path.getsize(self._output_file.name)
if bytes_have:
# Set ``Range`` header to resume the download
# TODO: Use "If-Range: mtime" to make sure it's fresh?
request_headers['Range'] = 'bytes=%d-' % bytes_have
self._resumed_from = bytes_have
def start(self, response):
"""
Initiate and return a stream for `response` body with progress
callback attached. Can be called only once.
:param response: Initiated response object with headers already fetched
:type response: requests.models.Response
:return: RawStream, output_file
"""
assert not self.status.time_started
# FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/jkbrzt/httpie/issues/423>
try:
total_size = int(response.headers['Content-Length'])
except (KeyError, ValueError, TypeError):
total_size = None
if self._output_file:
if self._resume and response.status_code == PARTIAL_CONTENT:
total_size = parse_content_range(
response.headers.get('Content-Range'),
self._resumed_from
)
else:
self._resumed_from = 0
try:
self._output_file.seek(0)
self._output_file.truncate()
except IOError:
pass # stdout
else:
# TODO: Should the filename be taken from response.history[0].url?
# Output file not specified. Pick a name that doesn't exist yet.
filename = None
if 'Content-Disposition' in response.headers:
filename = filename_from_content_disposition(
response.headers['Content-Disposition'])
if not filename:
filename = filename_from_url(
url=response.url,
content_type=response.headers.get('Content-Type'),
)
self._output_file = open(get_unique_filename(filename), mode='a+b')
self.status.started(
resumed_from=self._resumed_from,
total_size=total_size
)
stream = RawStream(
msg=HTTPResponse(response),
with_headers=False,
with_body=True,
on_body_chunk_downloaded=self.chunk_downloaded,
chunk_size=1024 * 8
)
self._progress_reporter.output.write(
'Downloading %sto "%s"\n' % (
(humanize_bytes(total_size) + ' '
if total_size is not None
else ''),
self._output_file.name
)
)
self._progress_reporter.start()
return stream, self._output_file
def finish(self):
assert not self.finished
self.finished = True
self.status.finished()
def failed(self):
self._progress_reporter.stop()
@property
def interrupted(self):
return (
self.finished and
self.status.total_size and
self.status.total_size != self.status.downloaded
)
def chunk_downloaded(self, chunk):
"""
A download progress callback.
:param chunk: A chunk of response body data that has just
been downloaded and written to the output.
:type chunk: bytes
"""
self.status.chunk_downloaded(len(chunk))
class Status(object):
"""Holds details about the downland status."""
def __init__(self):
self.downloaded = 0
self.total_size = None
self.resumed_from = 0
self.time_started = None
self.time_finished = None
def started(self, resumed_from=0, total_size=None):
assert self.time_started is None
self.total_size = total_size
self.downloaded = self.resumed_from = resumed_from
self.time_started = time()
def chunk_downloaded(self, size):
assert self.time_finished is None
self.downloaded += size
@property
def has_finished(self):
return self.time_finished is not None
def finished(self):
assert self.time_started is not None
assert self.time_finished is None
self.time_finished = time()
class ProgressReporterThread(threading.Thread):
"""
Reports download progress based on its status.
Uses threading to periodically update the status (speed, ETA, etc.).
"""
def __init__(self, status, output, tick=.1, update_interval=1):
"""
:type status: Status
:type output: file
"""
super(ProgressReporterThread, self).__init__()
self.status = status
self.output = output
self._tick = tick
self._update_interval = update_interval
self._spinner_pos = 0
self._status_line = ''
self._prev_bytes = 0
self._prev_time = time()
self._should_stop = threading.Event()
def stop(self):
"""Stop reporting on next tick."""
self._should_stop.set()
def run(self):
while not self._should_stop.is_set():
if self.status.has_finished:
self.sum_up()
break
self.report_speed()
sleep(self._tick)
def report_speed(self):
now = time()
if now - self._prev_time >= self._update_interval:
downloaded = self.status.downloaded
try:
speed = ((downloaded - self._prev_bytes) /
(now - self._prev_time))
except ZeroDivisionError:
speed = 0
if not self.status.total_size:
self._status_line = PROGRESS_NO_CONTENT_LENGTH.format(
downloaded=humanize_bytes(downloaded),
speed=humanize_bytes(speed),
)
else:
try:
percentage = downloaded / self.status.total_size * 100
except ZeroDivisionError:
percentage = 0
if not speed:
eta = '-:--:--'
else:
s = int((self.status.total_size - downloaded) / speed)
h, s = divmod(s, 60 * 60)
m, s = divmod(s, 60)
eta = '{0}:{1:0>2}:{2:0>2}'.format(h, m, s)
self._status_line = PROGRESS.format(
percentage=percentage,
downloaded=humanize_bytes(downloaded),
speed=humanize_bytes(speed),
eta=eta,
)
self._prev_time = now
self._prev_bytes = downloaded
self.output.write(
CLEAR_LINE +
' ' +
SPINNER[self._spinner_pos] +
' ' +
self._status_line
)
self.output.flush()
self._spinner_pos = (self._spinner_pos + 1
if self._spinner_pos + 1 != len(SPINNER)
else 0)
def sum_up(self):
actually_downloaded = (
self.status.downloaded - self.status.resumed_from)
time_taken = self.status.time_finished - self.status.time_started
self.output.write(CLEAR_LINE)
try:
speed = actually_downloaded / time_taken
except ZeroDivisionError:
# Either time is 0 (not all systems provide `time.time`
# with a better precision than 1 second), and/or nothing
# has been downloaded.
speed = actually_downloaded
self.output.write(SUMMARY.format(
downloaded=humanize_bytes(actually_downloaded),
total=(self.status.total_size and
humanize_bytes(self.status.total_size)),
speed=humanize_bytes(speed),
time=time_taken,
))
self.output.flush()

View File

@ -1,74 +0,0 @@
from requests.compat import urlparse
class HTTPMessage(object):
"""Model representing an HTTP message."""
def __init__(self, line, headers, body, content_type=None):
# {Request,Status}-Line
self.line = line
self.headers = headers
self.body = body
self.content_type = content_type
def from_request(request):
"""Make an `HTTPMessage` from `requests.models.Request`."""
url = urlparse(request.url)
request_headers = dict(request.headers)
if 'Host' not in request_headers:
request_headers['Host'] = url.netloc
try:
body = request.data
except AttributeError:
# requests < 0.12.1
body = request._enc_data
return HTTPMessage(
line='{method} {path} HTTP/1.1'.format(
method=request.method,
path=url.path or '/'),
headers='\n'.join(str('%s: %s') % (name, value)
for name, value
in request_headers.items()),
body=body,
content_type=request_headers.get('Content-Type')
)
def from_response(response):
"""Make an `HTTPMessage` from `requests.models.Response`."""
encoding = response.encoding or 'ISO-8859-1'
original = response.raw._original_response
response_headers = response.headers
return HTTPMessage(
line='HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status, reason=original.reason),
headers=str(original.msg),
body=response.content.decode(encoding) if response.content else '',
content_type=response_headers.get('Content-Type'))
def format(message, prettifier=None,
with_headers=True, with_body=True):
"""Return a `unicode` representation of `message`. """
pretty = prettifier is not None
bits = []
if with_headers:
bits.append(message.line)
bits.append(message.headers)
if pretty:
bits = [prettifier.headers('\n'.join(bits))]
if with_body and message.body:
bits.append('\n')
if with_body and message.body:
if pretty and message.content_type:
bits.append(prettifier.body(message.body, message.content_type))
else:
bits.append(message.body)
return '\n'.join(bit.strip() for bit in bits)

755
httpie/input.py Normal file
View File

@ -0,0 +1,755 @@
"""Parsing and processing of CLI input (args, auth credentials, files, stdin).
"""
import os
import ssl
import sys
import re
import errno
import mimetypes
import getpass
from io import BytesIO
from collections import namedtuple, Iterable
# noinspection PyCompatibility
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
# TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jkbrzt/httpie/issues/130
from httpie.plugins import plugin_manager
from requests.structures import CaseInsensitiveDict
from httpie.compat import OrderedDict, urlsplit, str, is_pypy, is_py27
from httpie.sessions import VALID_SESSION_NAME_PATTERN
from httpie.utils import load_json_preserve_order
# ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
# <http://tools.ietf.org/html/rfc3986#section-3.1>
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
HTTP_POST = 'POST'
HTTP_GET = 'GET'
# Various separators used in args
SEP_HEADERS = ':'
SEP_HEADERS_EMPTY = ';'
SEP_CREDENTIALS = ':'
SEP_PROXY = ':'
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
SEP_DATA_EMBED_FILE = '=@'
SEP_DATA_EMBED_RAW_JSON_FILE = ':=@'
SEP_QUERY = '=='
# Separators that become request data
SEP_GROUP_DATA_ITEMS = frozenset([
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES,
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE
])
# Separators for items whose value is a filename to be embedded
SEP_GROUP_DATA_EMBED_ITEMS = frozenset([
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE,
])
# Separators for raw JSON items
SEP_GROUP_RAW_JSON_ITEMS = frozenset([
SEP_DATA_RAW_JSON,
SEP_DATA_EMBED_RAW_JSON_FILE,
])
# Separators allowed in ITEM arguments
SEP_GROUP_ALL_ITEMS = frozenset([
SEP_HEADERS,
SEP_HEADERS_EMPTY,
SEP_QUERY,
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES,
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE,
])
# Output options
OUT_REQ_HEAD = 'H'
OUT_REQ_BODY = 'B'
OUT_RESP_HEAD = 'h'
OUT_RESP_BODY = 'b'
OUTPUT_OPTIONS = frozenset([
OUT_REQ_HEAD,
OUT_REQ_BODY,
OUT_RESP_HEAD,
OUT_RESP_BODY
])
# Pretty
PRETTY_MAP = {
'all': ['format', 'colors'],
'colors': ['colors'],
'format': ['format'],
'none': []
}
PRETTY_STDOUT_TTY_ONLY = object()
# Defaults
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
SSL_VERSION_ARG_MAPPING = {
'ssl2.3': 'PROTOCOL_SSLv23',
'ssl3': 'PROTOCOL_SSLv3',
'tls1': 'PROTOCOL_TLSv1',
'tls1.1': 'PROTOCOL_TLSv1_1',
'tls1.2': 'PROTOCOL_TLSv1_2',
}
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`.
Handles all input (CLI args, file args, stdin), applies defaults,
and performs extra validation.
"""
def __init__(self, *args, **kwargs):
kwargs['add_help'] = False
super(HTTPieArgumentParser, self).__init__(*args, **kwargs)
# noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None):
self.env = env
self.args, no_options = super(HTTPieArgumentParser, self)\
.parse_known_args(args, namespace)
if self.args.debug:
self.args.traceback = True
# Arguments processing and environment setup.
self._apply_no_options(no_options)
self._validate_download_options()
self._setup_standard_streams()
self._process_output_options()
self._process_pretty_options()
self._guess_method()
self._parse_items()
if not self.args.ignore_stdin and not env.stdin_isatty:
self._body_from_file(self.env.stdin)
if not URL_SCHEME_RE.match(self.args.url):
scheme = self.args.default_scheme + "://"
# See if we're using curl style shorthand for localhost (:3000/foo)
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
if shorthand:
port = shorthand.group(1)
rest = shorthand.group(2)
self.args.url = scheme + 'localhost'
if port:
self.args.url += ':' + port
self.args.url += rest
else:
self.args.url = scheme + self.args.url
self._process_auth()
return self.args
# noinspection PyShadowingBuiltins
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
sys.stdout: self.env.stdout,
sys.stderr: self.env.stderr,
None: self.env.stderr
}.get(file, file)
if not hasattr(file, 'buffer') and isinstance(message, str):
message = message.encode(self.env.stdout_encoding)
super(HTTPieArgumentParser, self)._print_message(message, file)
def _setup_standard_streams(self):
"""
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
"""
self.args.output_file_specified = bool(self.args.output_file)
if self.args.download:
# FIXME: Come up with a cleaner solution.
if not self.args.output_file and not self.env.stdout_isatty:
# Use stdout as the download output file.
self.args.output_file = self.env.stdout
# With `--download`, we write everything that would normally go to
# `stdout` to `stderr` instead. Let's replace the stream so that
# we don't have to use many `if`s throughout the codebase.
# The response body will be treated separately.
self.env.stdout = self.env.stderr
self.env.stdout_isatty = self.env.stderr_isatty
elif self.args.output_file:
# When not `--download`ing, then `--output` simply replaces
# `stdout`. The file is opened for appending, which isn't what
# we want in this case.
self.args.output_file.seek(0)
try:
self.args.output_file.truncate()
except IOError as e:
if e.errno == errno.EINVAL:
# E.g. /dev/null on Linux.
pass
else:
raise
self.env.stdout = self.args.output_file
self.env.stdout_isatty = False
def _process_auth(self):
# TODO: refactor
self.args.auth_plugin = None
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
auth_type_set = self.args.auth_type is not None
url = urlsplit(self.args.url)
if self.args.auth is None and not auth_type_set:
if url.username is not None:
# Handle http://username:password@hostname/
username = url.username
password = url.password or ''
self.args.auth = AuthCredentials(
key=username,
value=password,
sep=SEP_CREDENTIALS,
orig=SEP_CREDENTIALS.join([username, password])
)
if self.args.auth is not None or auth_type_set:
if not self.args.auth_type:
self.args.auth_type = default_auth_plugin.auth_type
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
if plugin.auth_require and self.args.auth is None:
self.error('--auth required')
plugin.raw_auth = self.args.auth
self.args.auth_plugin = plugin
already_parsed = isinstance(self.args.auth, AuthCredentials)
if self.args.auth is None or not plugin.auth_parse:
self.args.auth = plugin.get_auth()
else:
if already_parsed:
# from the URL
credentials = self.args.auth
else:
credentials = parse_auth(self.args.auth)
if (not credentials.has_password() and
plugin.prompt_password):
if self.args.ignore_stdin:
# Non-tty stdin read by now
self.error(
'Unable to prompt for passwords because'
' --ignore-stdin is set.'
)
credentials.prompt_password(url.netloc)
self.args.auth = plugin.get_auth(
username=credentials.key,
password=credentials.value,
)
def _apply_no_options(self, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g.,
specified in config.
"""
invalid = []
for option in no_options:
if not option.startswith('--no-'):
invalid.append(option)
continue
# --no-option => --option
inverted = '--' + option[5:]
for action in self._actions:
if inverted in action.option_strings:
setattr(self.args, action.dest, action.default)
break
else:
invalid.append(option)
if invalid:
msg = 'unrecognized arguments: %s'
self.error(msg % ' '.join(invalid))
def _body_from_file(self, fd):
"""There can only be one source of request data.
Bytes are always read.
"""
if self.args.data:
self.error('Request body (from stdin or a file) and request '
'data (key=value) cannot be mixed.')
self.args.data = getattr(fd, 'buffer', fd).read()
def _guess_method(self):
"""Set `args.method` if not specified to either POST or GET
based on whether the request has data or not.
"""
if self.args.method is None:
# Invoked as `http URL'.
assert not self.args.items
if not self.args.ignore_stdin and not self.env.stdin_isatty:
self.args.method = HTTP_POST
else:
self.args.method = HTTP_GET
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
elif not re.match('^[a-zA-Z]+$', self.args.method):
# Invoked as `http URL item+'. The URL is now in `args.method`
# and the first ITEM is now incorrectly in `args.url`.
try:
# Parse the URL as an ITEM and store it as the first ITEM arg.
self.args.items.insert(0, KeyValueArgType(
*SEP_GROUP_ALL_ITEMS).__call__(self.args.url))
except ArgumentTypeError as e:
if self.args.traceback:
raise
self.error(e.args[0])
else:
# Set the URL correctly
self.args.url = self.args.method
# Infer the method
has_data = (
(not self.args.ignore_stdin and
not self.env.stdin_isatty) or
any(item.sep in SEP_GROUP_DATA_ITEMS
for item in self.args.items)
)
self.args.method = HTTP_POST if has_data else HTTP_GET
def _parse_items(self):
"""Parse `args.items` into `args.headers`, `args.data`, `args.params`,
and `args.files`.
"""
try:
items = parse_items(
items=self.args.items,
data_class=ParamsDict if self.args.form else OrderedDict
)
except ParseError as e:
if self.args.traceback:
raise
self.error(e.args[0])
else:
self.args.headers = items.headers
self.args.data = items.data
self.args.files = items.files
self.args.params = items.params
if self.args.files and not self.args.form:
# `http url @/path/to/file`
file_fields = list(self.args.files.keys())
if file_fields != ['']:
self.error(
'Invalid file fields (perhaps you meant --form?): %s'
% ','.join(file_fields))
fn, fd, ct = self.args.files['']
self.args.files = {}
self._body_from_file(fd)
if 'Content-Type' not in self.args.headers:
content_type = get_content_type(fn)
if content_type:
self.args.headers['Content-Type'] = content_type
def _process_output_options(self):
"""Apply defaults to output options, or validate the provided ones.
The default output options are stdout-type-sensitive.
"""
def check_options(value, option):
unknown = set(value) - OUTPUT_OPTIONS
if unknown:
self.error('Unknown output options: {0}={1}'.format(
option,
','.join(unknown)
))
if self.args.verbose:
self.args.all = True
if self.args.output_options is None:
if self.args.verbose:
self.args.output_options = ''.join(OUTPUT_OPTIONS)
else:
self.args.output_options = (
OUTPUT_OPTIONS_DEFAULT
if self.env.stdout_isatty
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
)
if self.args.output_options_history is None:
self.args.output_options_history = self.args.output_options
check_options(self.args.output_options, '--print')
check_options(self.args.output_options_history, '--history-print')
if self.args.download and OUT_RESP_BODY in self.args.output_options:
# Response body is always downloaded with --download and it goes
# through a different routine, so we remove it.
self.args.output_options = str(
set(self.args.output_options) - set(OUT_RESP_BODY))
def _process_pretty_options(self):
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
self.args.prettify = PRETTY_MAP[
'all' if self.env.stdout_isatty else 'none']
elif (self.args.prettify and self.env.is_windows and
self.args.output_file):
self.error('Only terminal output can be colorized on Windows.')
else:
# noinspection PyTypeChecker
self.args.prettify = PRETTY_MAP[self.args.prettify]
def _validate_download_options(self):
if not self.args.download:
if self.args.download_resume:
self.error('--continue only works with --download')
if self.args.download_resume and not (
self.args.download and self.args.output_file):
self.error('--continue requires --output to be specified')
class ParseError(Exception):
pass
class KeyValue(object):
"""Base key-value pair parsed from CLI."""
def __init__(self, key, value, sep, orig):
self.key = key
self.value = value
self.sep = sep
self.orig = orig
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __repr__(self):
return repr(self.__dict__)
class SessionNameValidator(object):
def __init__(self, error_message):
self.error_message = error_message
def __call__(self, value):
# Session name can be a path or just a name.
if (os.path.sep not in value and
not VALID_SESSION_NAME_PATTERN.search(value)):
raise ArgumentError(None, self.error_message)
return value
class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`.
Parses a key-value arg and constructs a `KeyValue` instance.
Used for headers, form data, and other key-value pair types.
"""
key_value_class = KeyValue
def __init__(self, *separators):
self.separators = separators
self.special_characters = set('\\')
for separator in separators:
self.special_characters.update(separator)
def __call__(self, string):
"""Parse `string` and return `self.key_value_class()` instance.
The best of `self.separators` is determined (first found, longest).
Back slash escaped characters aren't considered as separators
(or parts thereof). Literal back slash characters have to be escaped
as well (r'\\').
"""
class Escaped(str):
"""Represents an escaped character."""
def tokenize(string):
"""Tokenize `string`. There are only two token types - strings
and escaped characters:
tokenize(r'foo\=bar\\baz')
=> ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
"""
tokens = ['']
characters = iter(string)
for char in characters:
if char == '\\':
char = next(characters, '')
if char not in self.special_characters:
tokens[-1] += '\\' + char
else:
tokens.extend([Escaped(char), ''])
else:
tokens[-1] += char
return tokens
tokens = tokenize(string)
# Sorting by length ensures that the longest one will be
# chosen as it will overwrite any shorter ones starting
# at the same position in the `found` dictionary.
separators = sorted(self.separators, key=len)
for i, token in enumerate(tokens):
if isinstance(token, Escaped):
continue
found = {}
for sep in separators:
pos = token.find(sep)
if pos != -1:
found[pos] = sep
if found:
# Starting first, longest separator found.
sep = found[min(found.keys())]
key, value = token.split(sep, 1)
# Any preceding tokens are part of the key.
key = ''.join(tokens[:i]) + key
# Any following tokens are part of the value.
value += ''.join(tokens[i + 1:])
break
else:
raise ArgumentTypeError(
u'"%s" is not a valid value' % string)
return self.key_value_class(
key=key, value=value, sep=sep, orig=string)
class AuthCredentials(KeyValue):
"""Represents parsed credentials."""
def _getpass(self, prompt):
# To allow mocking.
return getpass.getpass(str(prompt))
def has_password(self):
return self.value is not None
def prompt_password(self, host):
try:
self.value = self._getpass(
'http: password for %s@%s: ' % (self.key, host))
except (EOFError, KeyboardInterrupt):
sys.stderr.write('\n')
sys.exit(0)
class AuthCredentialsArgType(KeyValueArgType):
"""A key-value arg type that parses credentials."""
key_value_class = AuthCredentials
def __call__(self, string):
"""Parse credentials from `string`.
("username" or "username:password").
"""
try:
return super(AuthCredentialsArgType, self).__call__(string)
except ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
key=string,
value=None,
sep=SEP_CREDENTIALS,
orig=string
)
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
class RequestItemsDict(OrderedDict):
"""Multi-value dict for URL parameters and form data."""
if is_pypy and is_py27:
# Manually set keys when initialized with an iterable as PyPy
# doesn't call __setitem__ in such case (pypy3 does).
def __init__(self, *args, **kwargs):
if len(args) == 1 and isinstance(args[0], Iterable):
super(RequestItemsDict, self).__init__(**kwargs)
for k, v in args[0]:
self[k] = v
else:
super(RequestItemsDict, self).__init__(*args, **kwargs)
# noinspection PyMethodOverriding
def __setitem__(self, key, value):
""" If `key` is assigned more than once, `self[key]` holds a
`list` of all the values.
This allows having multiple fields with the same name in form
data and URL params.
"""
assert not isinstance(value, list)
if key not in self:
super(RequestItemsDict, self).__setitem__(key, value)
else:
if not isinstance(self[key], list):
super(RequestItemsDict, self).__setitem__(key, [self[key]])
self[key].append(value)
class ParamsDict(RequestItemsDict):
pass
class DataDict(RequestItemsDict):
def items(self):
for key, values in super(RequestItemsDict, self).items():
if not isinstance(values, list):
values = [values]
for value in values:
yield key, value
RequestItems = namedtuple('RequestItems',
['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,
headers_class=CaseInsensitiveDict,
data_class=OrderedDict,
files_class=DataDict,
params_class=ParamsDict):
"""Parse `KeyValue` `items` into `data`, `headers`, `files`,
and `params`.
"""
headers = []
data = []
files = []
params = []
for item in items:
value = item.value
if item.sep == SEP_HEADERS:
if value == '':
# No value => unset the header
value = None
target = headers
elif item.sep == SEP_HEADERS_EMPTY:
if item.value:
raise ParseError(
'Invalid item "%s" '
'(to specify an empty header use `Header;`)'
% item.orig
)
target = headers
elif item.sep == SEP_QUERY:
target = params
elif item.sep == SEP_FILES:
try:
with open(os.path.expanduser(value), 'rb') as f:
value = (os.path.basename(value),
BytesIO(f.read()),
get_content_type(value))
except IOError as e:
raise ParseError('"%s": %s' % (item.orig, e))
target = files
elif item.sep in SEP_GROUP_DATA_ITEMS:
if item.sep in SEP_GROUP_DATA_EMBED_ITEMS:
try:
with open(os.path.expanduser(value), 'rb') as f:
value = f.read().decode('utf8')
except IOError as e:
raise ParseError('"%s": %s' % (item.orig, e))
except UnicodeDecodeError:
raise ParseError(
'"%s": cannot embed the content of "%s",'
' not a UTF8 or ASCII-encoded text file'
% (item.orig, item.value)
)
if item.sep in SEP_GROUP_RAW_JSON_ITEMS:
try:
value = load_json_preserve_order(value)
except ValueError as e:
raise ParseError('"%s": %s' % (item.orig, e))
target = data
else:
raise TypeError(item)
target.append((item.key, value))
return RequestItems(headers_class(headers),
data_class(data),
files_class(files),
params_class(params))
def readable_file_arg(filename):
try:
open(filename, 'rb')
except IOError as ex:
raise ArgumentTypeError('%s: %s' % (filename, ex.args[1]))
return filename

141
httpie/models.py Normal file
View File

@ -0,0 +1,141 @@
from httpie.compat import urlsplit, str
class HTTPMessage(object):
"""Abstract class for HTTP messages."""
def __init__(self, orig):
self._orig = orig
def iter_body(self, chunk_size):
"""Return an iterator over the body."""
raise NotImplementedError()
def iter_lines(self, chunk_size):
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
raise NotImplementedError()
@property
def headers(self):
"""Return a `str` with the message's headers."""
raise NotImplementedError()
@property
def encoding(self):
"""Return a `str` with the message's encoding, if known."""
raise NotImplementedError()
@property
def body(self):
"""Return a `bytes` with the message's body."""
raise NotImplementedError()
@property
def content_type(self):
"""Return the message content type."""
ct = self._orig.headers.get('Content-Type', '')
if not isinstance(ct, str):
ct = ct.decode('utf8')
return ct
class HTTPResponse(HTTPMessage):
"""A :class:`requests.models.Response` wrapper."""
def iter_body(self, chunk_size=1):
return self._orig.iter_content(chunk_size=chunk_size)
def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
# noinspection PyProtectedMember
@property
def headers(self):
original = self._orig.raw._original_response
version = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2',
}[original.version]
status_line = 'HTTP/{version} {status} {reason}'.format(
version=version,
status=original.status,
reason=original.reason
)
headers = [status_line]
try:
# `original.msg` is a `http.client.HTTPMessage` on Python 3
# `_headers` is a 2-tuple
headers.extend(
'%s: %s' % header for header in original.msg._headers)
except AttributeError:
# and a `httplib.HTTPMessage` on Python 2.x
# `headers` is a list of `name: val<CRLF>`.
headers.extend(h.strip() for h in original.msg.headers)
return '\r\n'.join(headers)
@property
def encoding(self):
return self._orig.encoding or 'utf8'
@property
def body(self):
# Only now the response body is fetched.
# Shouldn't be touched unless the body is actually needed.
return self._orig.content
class HTTPRequest(HTTPMessage):
"""A :class:`requests.models.Request` wrapper."""
def iter_body(self, chunk_size):
yield self.body
def iter_lines(self, chunk_size):
yield self.body, b''
@property
def headers(self):
url = urlsplit(self._orig.url)
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method,
path=url.path or '/',
query='?' + url.query if url.query else ''
)
headers = dict(self._orig.headers)
if 'Host' not in self._orig.headers:
headers['Host'] = url.netloc.split('@')[-1]
headers = [
'%s: %s' % (
name,
value if isinstance(value, str) else value.decode('utf8')
)
for name, value in headers.items()
]
headers.insert(0, request_line)
headers = '\r\n'.join(headers).strip()
if isinstance(headers, bytes):
# Python < 3
headers = headers.decode('utf8')
return headers
@property
def encoding(self):
return 'utf8'
@property
def body(self):
body = self._orig.body
if isinstance(body, str):
# Happens with JSON/form request data parsed from the command line.
body = body.encode('utf8')
return body or b''

View File

View File

View File

@ -0,0 +1,241 @@
from __future__ import absolute_import
import json
import pygments.lexer
import pygments.token
import pygments.styles
import pygments.lexers
import pygments.style
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.lexers.special import TextLexer
from pygments.util import ClassNotFound
from httpie.compat import is_windows
from httpie.plugins import FormatterPlugin
AVAILABLE_STYLES = set(pygments.styles.STYLE_MAP.keys())
AVAILABLE_STYLES.add('solarized')
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):
"""
Colorize using Pygments
This processor that applies syntax highlighting to the headers,
and also to the body if its content type is recognized.
"""
group_name = 'colors'
def __init__(self, env, explicit_json=False,
color_scheme=DEFAULT_STYLE, **kwargs):
super(ColorFormatter, self).__init__(**kwargs)
if not env.colors:
self.enabled = False
return
# --json, -j
self.explicit_json = explicit_json
try:
style_class = pygments.styles.get_style_by_name(color_scheme)
except ClassNotFound:
style_class = Solarized256Style
if env.colors == 256:
fmt_class = Terminal256Formatter
else:
fmt_class = TerminalFormatter
self.formatter = fmt_class(style=style_class)
def format_headers(self, headers):
return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
def format_body(self, body, mime):
lexer = self.get_lexer(mime, body)
if lexer:
body = pygments.highlight(body, lexer, self.formatter)
return body.strip()
def get_lexer(self, mime, body):
return get_lexer(
mime=mime,
explicit_json=self.explicit_json,
body=body,
)
def get_lexer(mime, explicit_json=False, body=''):
# Build candidate mime type and lexer names.
mime_types, lexer_names = [mime], []
type_, subtype = mime.split('/', 1)
if '+' not in subtype:
lexer_names.append(subtype)
else:
subtype_name, subtype_suffix = subtype.split('+', 1)
lexer_names.extend([subtype_name, subtype_suffix])
mime_types.extend([
'%s/%s' % (type_, subtype_name),
'%s/%s' % (type_, subtype_suffix)
])
# As a last resort, if no lexer feels responsible, and
# the subtype contains 'json', take the JSON lexer
if 'json' in subtype:
lexer_names.append('json')
# Try to resolve the right lexer.
lexer = None
for mime_type in mime_types:
try:
lexer = pygments.lexers.get_lexer_for_mimetype(mime_type)
break
except ClassNotFound:
pass
else:
for name in lexer_names:
try:
lexer = pygments.lexers.get_lexer_by_name(name)
except ClassNotFound:
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
class HTTPLexer(pygments.lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between
their names and values than the original one bundled with Pygments
(:class:`pygments.lexers.text import HttpLexer`), especially when
Solarized color scheme is used.
"""
name = 'HTTP'
aliases = ['http']
filenames = ['*.http']
tokens = {
'root': [
# Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
pygments.lexer.bygroups(
pygments.token.Name.Function,
pygments.token.Text,
pygments.token.Name.Namespace,
pygments.token.Text,
pygments.token.Keyword.Reserved,
pygments.token.Operator,
pygments.token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
pygments.lexer.bygroups(
pygments.token.Keyword.Reserved, # 'HTTP'
pygments.token.Operator, # '/'
pygments.token.Number, # Version
pygments.token.Text,
pygments.token.Number, # Status code
pygments.token.Text,
pygments.token.Name.Exception, # Reason
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
pygments.token.Name.Attribute, # Name
pygments.token.Text,
pygments.token.Operator, # Colon
pygments.token.Text,
pygments.token.String # Value
))
]
}
class Solarized256Style(pygments.style.Style):
"""
solarized256
------------
A Pygments style inspired by Solarized's 256 color mode.
:copyright: (c) 2011 by Hank Gay, (c) 2012 by John Mastro.
:license: BSD, see LICENSE for more details.
"""
BASE03 = "#1c1c1c"
BASE02 = "#262626"
BASE01 = "#4e4e4e"
BASE00 = "#585858"
BASE0 = "#808080"
BASE1 = "#8a8a8a"
BASE2 = "#d7d7af"
BASE3 = "#ffffd7"
YELLOW = "#af8700"
ORANGE = "#d75f00"
RED = "#af0000"
MAGENTA = "#af005f"
VIOLET = "#5f5faf"
BLUE = "#0087ff"
CYAN = "#00afaf"
GREEN = "#5f8700"
background_color = BASE03
styles = {
pygments.token.Keyword: GREEN,
pygments.token.Keyword.Constant: ORANGE,
pygments.token.Keyword.Declaration: BLUE,
pygments.token.Keyword.Namespace: ORANGE,
pygments.token.Keyword.Reserved: BLUE,
pygments.token.Keyword.Type: RED,
pygments.token.Name.Attribute: BASE1,
pygments.token.Name.Builtin: BLUE,
pygments.token.Name.Builtin.Pseudo: BLUE,
pygments.token.Name.Class: BLUE,
pygments.token.Name.Constant: ORANGE,
pygments.token.Name.Decorator: BLUE,
pygments.token.Name.Entity: ORANGE,
pygments.token.Name.Exception: YELLOW,
pygments.token.Name.Function: BLUE,
pygments.token.Name.Tag: BLUE,
pygments.token.Name.Variable: BLUE,
pygments.token.String: CYAN,
pygments.token.String.Backtick: BASE01,
pygments.token.String.Char: CYAN,
pygments.token.String.Doc: CYAN,
pygments.token.String.Escape: RED,
pygments.token.String.Heredoc: CYAN,
pygments.token.String.Regex: RED,
pygments.token.Number: CYAN,
pygments.token.Operator: BASE1,
pygments.token.Operator.Word: GREEN,
pygments.token.Comment: BASE01,
pygments.token.Comment.Preproc: GREEN,
pygments.token.Comment.Special: GREEN,
pygments.token.Generic.Deleted: CYAN,
pygments.token.Generic.Emph: 'italic',
pygments.token.Generic.Error: RED,
pygments.token.Generic.Heading: ORANGE,
pygments.token.Generic.Inserted: GREEN,
pygments.token.Generic.Strong: 'bold',
pygments.token.Generic.Subheading: ORANGE,
pygments.token.Token: BASE1,
pygments.token.Token.Other: ORANGE,
}

View File

@ -0,0 +1,14 @@
from httpie.plugins import FormatterPlugin
class HeadersFormatter(FormatterPlugin):
def format_headers(self, headers):
"""
Sorts headers by name while retaining relative
order of multiple headers with the same name.
"""
lines = headers.splitlines()
headers = sorted(lines[1:], key=lambda h: h.split(':')[0])
return '\r\n'.join(lines[:1] + headers)

View File

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

View File

@ -0,0 +1,50 @@
import re
from httpie.plugins import plugin_manager
from httpie.context import Environment
MIME_RE = re.compile(r'^[^/]+/[^/]+$')
def is_valid_mime(mime):
return mime and MIME_RE.match(mime)
class Conversion(object):
def get_converter(self, mime):
if is_valid_mime(mime):
for converter_class in plugin_manager.get_converters():
if converter_class.supports(mime):
return converter_class(mime)
class Formatting(object):
"""A delegate class that invokes the actual processors."""
def __init__(self, groups, env=Environment(), **kwargs):
"""
:param groups: names of processor groups to be applied
:param env: Environment
:param kwargs: additional keyword arguments for processors
"""
available_plugins = plugin_manager.get_formatters_grouped()
self.enabled_plugins = []
for group in groups:
for cls in available_plugins[group]:
p = cls(env=env, **kwargs)
if p.enabled:
self.enabled_plugins.append(p)
def format_headers(self, headers):
for p in self.enabled_plugins:
headers = p.format_headers(headers)
return headers
def format_body(self, content, mime):
if is_valid_mime(mime):
for p in self.enabled_plugins:
content = p.format_body(content, mime)
return content

297
httpie/output/streams.py Normal file
View File

@ -0,0 +1,297 @@
from itertools import chain
from functools import partial
from httpie.compat import str
from httpie.context import Environment
from httpie.models import HTTPRequest, HTTPResponse
from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY)
from httpie.output.processing import Formatting, Conversion
BINARY_SUPPRESSED_NOTICE = (
b'\n'
b'+-----------------------------------------+\n'
b'| NOTE: binary data not shown in terminal |\n'
b'+-----------------------------------------+'
)
class BinarySuppressedError(Exception):
"""An error indicating that the body is binary and won't be written,
e.g., for terminal output)."""
message = BINARY_SUPPRESSED_NOTICE
def write_stream(stream, outfile, flush):
"""Write the output stream."""
try:
# Writing bytes so we use the buffer interface (Python 3).
buf = outfile.buffer
except AttributeError:
buf = outfile
for chunk in stream:
buf.write(chunk)
if flush:
outfile.flush()
def write_stream_with_colors_win_py3(stream, outfile, flush):
"""Like `write`, but colorized chunks are written as text
directly to `outfile` to ensure it gets processed by colorama.
Applies only to Windows with Python 3 and colorized terminal output.
"""
color = b'\x1b['
encoding = outfile.encoding
for chunk in stream:
if color in chunk:
outfile.write(chunk.decode(encoding))
else:
outfile.buffer.write(chunk)
if flush:
outfile.flush()
def build_output_stream(args, env, request, response, output_options):
"""Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks.
"""
req_h = OUT_REQ_HEAD in output_options
req_b = OUT_REQ_BODY in output_options
resp_h = OUT_RESP_HEAD in output_options
resp_b = OUT_RESP_BODY in output_options
req = req_h or req_b
resp = resp_h or resp_b
output = []
Stream = get_stream_type(env, args)
if req:
output.append(Stream(
msg=HTTPRequest(request),
with_headers=req_h,
with_body=req_b))
if req_b and resp:
# Request/Response separator.
output.append([b'\n\n'])
if resp:
output.append(Stream(
msg=HTTPResponse(response),
with_headers=resp_h,
with_body=resp_b))
if env.stdout_isatty and resp_b:
# Ensure a blank line after the response body.
# For terminal output only.
output.append([b'\n\n'])
return chain(*output)
def get_stream_type(env, args):
"""Pick the right stream type based on `env` and `args`.
Wrap it in a partial with the type-specific args so that
we don't need to think what stream we are dealing with.
"""
if not env.stdout_isatty and not args.prettify:
Stream = partial(
RawStream,
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
if args.stream
else RawStream.CHUNK_SIZE
)
elif args.prettify:
Stream = partial(
PrettyStream if args.stream else BufferedPrettyStream,
env=env,
conversion=Conversion(),
formatting=Formatting(
env=env,
groups=args.prettify,
color_scheme=args.style,
explicit_json=args.json,
),
)
else:
Stream = partial(EncodedStream, env=env)
return Stream
class BaseStream(object):
"""Base HTTP message output stream class."""
def __init__(self, msg, with_headers=True, with_body=True,
on_body_chunk_downloaded=None):
"""
:param msg: a :class:`models.HTTPMessage` subclass
:param with_headers: if `True`, headers will be included
:param with_body: if `True`, body will be included
"""
assert with_headers or with_body
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
self.on_body_chunk_downloaded = on_body_chunk_downloaded
def get_headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('utf8')
def iter_body(self):
"""Return an iterator over the message body."""
raise NotImplementedError()
def __iter__(self):
"""Return an iterator over `self.msg`."""
if self.with_headers:
yield self.get_headers()
yield b'\r\n\r\n'
if self.with_body:
try:
for chunk in self.iter_body():
yield chunk
if self.on_body_chunk_downloaded:
self.on_body_chunk_downloaded(chunk)
except BinarySuppressedError as e:
if self.with_headers:
yield b'\n'
yield e.message
class RawStream(BaseStream):
"""The message is streamed in chunks with no processing."""
CHUNK_SIZE = 1024 * 100
CHUNK_SIZE_BY_LINE = 1
def __init__(self, chunk_size=CHUNK_SIZE, **kwargs):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def iter_body(self):
return self.msg.iter_body(self.chunk_size)
class EncodedStream(BaseStream):
"""Encoded HTTP message stream.
The message bytes are converted to an encoding suitable for
`self.env.stdout`. Unicode errors are replaced and binary data
is suppressed. The body is always streamed by line.
"""
CHUNK_SIZE = 1
def __init__(self, env=Environment(), **kwargs):
super(EncodedStream, self).__init__(**kwargs)
if env.stdout_isatty:
# Use the encoding supported by the terminal.
output_encoding = env.stdout_encoding
else:
# Preserve the message encoding.
output_encoding = self.msg.encoding
# Default to utf8 when unsure.
self.output_encoding = output_encoding or 'utf8'
def iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
yield line.decode(self.msg.encoding) \
.encode(self.output_encoding, 'replace') + lf
class PrettyStream(EncodedStream):
"""In addition to :class:`EncodedStream` behaviour, this stream applies
content processing.
Useful for long-lived HTTP responses that stream by lines
such as the Twitter streaming API.
"""
CHUNK_SIZE = 1
def __init__(self, conversion, formatting, **kwargs):
super(PrettyStream, self).__init__(**kwargs)
self.formatting = formatting
self.conversion = conversion
self.mime = self.msg.content_type.split(';')[0]
def get_headers(self):
return self.formatting.format_headers(
self.msg.headers).encode(self.output_encoding)
def iter_body(self):
first_chunk = True
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
for line, lf in iter_lines:
if b'\0' in line:
if first_chunk:
converter = self.conversion.get_converter(self.mime)
if converter:
body = bytearray()
# noinspection PyAssignmentToLoopOrWithParameter
for line, lf in chain([(line, lf)], iter_lines):
body.extend(line)
body.extend(lf)
self.mime, body = converter.convert(body)
assert isinstance(body, str)
yield self.process_body(body)
return
raise BinarySuppressedError()
yield self.process_body(line) + lf
first_chunk = False
def process_body(self, chunk):
if not isinstance(chunk, str):
# Text when a converter has been used,
# otherwise it will always be bytes.
chunk = chunk.decode(self.msg.encoding, 'replace')
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
return chunk.encode(self.output_encoding, 'replace')
class BufferedPrettyStream(PrettyStream):
"""The same as :class:`PrettyStream` except that the body is fully
fetched before it's processed.
Suitable regular HTTP responses.
"""
CHUNK_SIZE = 1024 * 10
def iter_body(self):
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
converter = None
body = bytearray()
for chunk in self.msg.iter_body(self.CHUNK_SIZE):
if not converter and b'\0' in chunk:
converter = self.conversion.get_converter(self.mime)
if not converter:
raise BinarySuppressedError()
body.extend(chunk)
if converter:
self.mime, body = converter.convert(body)
yield self.process_body(body)

View File

@ -0,0 +1,22 @@
"""
WARNING: The plugin API is still work in progress and will
probably be completely reworked by v1.0.0.
"""
from httpie.plugins.base import (
AuthPlugin, FormatterPlugin,
ConverterPlugin, TransportPlugin
)
from httpie.plugins.manager import PluginManager
from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin
from httpie.output.formatters.headers import HeadersFormatter
from httpie.output.formatters.json import JSONFormatter
from httpie.output.formatters.colors import ColorFormatter
plugin_manager = PluginManager()
plugin_manager.register(BasicAuthPlugin,
DigestAuthPlugin)
plugin_manager.register(HeadersFormatter,
JSONFormatter,
ColorFormatter)

118
httpie/plugins/base.py Normal file
View File

@ -0,0 +1,118 @@
class BasePlugin(object):
# The name of the plugin, eg. "My auth".
name = None
# Optional short description. Will be be shown in the help
# under --auth-type.
description = None
# This be set automatically once the plugin has been loaded.
package_name = None
class AuthPlugin(BasePlugin):
"""
Base auth plugin class.
See <https://github.com/jkbrzt/httpie-ntlm> for an example auth plugin.
See also `test_auth_plugins.py`
"""
# The value that should be passed to --auth-type
# to use this auth plugin. Eg. "my-auth"
auth_type = None
# Set to `False` to make it possible to invoke this auth
# plugin without requiring the user to specify credentials
# through `--auth, -a`.
auth_require = True
# By default the `-a` argument is parsed for `username:password`.
# Set this to `False` to disable the parsing and error handling.
auth_parse = True
# If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in.
prompt_password = True
# Will be set to the raw value of `-a` (if provided) before
# `get_auth()` gets called.
raw_auth = None
def get_auth(self, username=None, password=None):
"""
If `auth_parse` is set to `True`, then `username`
and `password` contain the parsed credentials.
Use `self.raw_auth` to access the raw value passed through
`--auth, -a`.
Return a ``requests.auth.AuthBase`` subclass instance.
"""
raise NotImplementedError()
class TransportPlugin(BasePlugin):
"""
http://docs.python-requests.org/en/latest/user/advanced/#transport-adapters
"""
# The URL prefix the adapter should be mount to.
prefix = None
def get_adapter(self):
"""
Return a ``requests.adapters.BaseAdapter`` subclass instance to be
mounted to ``self.prefix``.
"""
raise NotImplementedError()
class ConverterPlugin(object):
def __init__(self, mime):
self.mime = mime
def convert(self, content_bytes):
raise NotImplementedError
@classmethod
def supports(cls, mime):
raise NotImplementedError
class FormatterPlugin(object):
def __init__(self, **kwargs):
"""
:param env: an class:`Environment` instance
:param kwargs: additional keyword argument that some
processor might require.
"""
self.enabled = True
self.kwargs = kwargs
def format_headers(self, headers):
"""Return processed `headers`
:param headers: The headers as text.
"""
return headers
def format_body(self, content, mime):
"""Return processed `content`.
:param mime: E.g., 'application/atom+xml'.
:param content: The body content as text
"""
return content

51
httpie/plugins/builtin.py Normal file
View File

@ -0,0 +1,51 @@
from base64 import b64encode
import requests.auth
from httpie.plugins.base import AuthPlugin
# noinspection PyAbstractClass
class BuiltinAuthPlugin(AuthPlugin):
package_name = '(builtin)'
class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
def __call__(self, r):
"""
Override username/password serialization to allow unicode.
See https://github.com/jkbrzt/httpie/issues/212
"""
r.headers['Authorization'] = type(self).make_header(
self.username, self.password).encode('latin1')
return r
@staticmethod
def make_header(username, password):
credentials = u'%s:%s' % (username, password)
token = b64encode(credentials.encode('utf8')).strip().decode('latin1')
return 'Basic %s' % token
class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth'
auth_type = 'basic'
# noinspection PyMethodOverriding
def get_auth(self, username, password):
return HTTPBasicAuth(username, password)
class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth'
auth_type = 'digest'
# noinspection PyMethodOverriding
def get_auth(self, username, password):
return requests.auth.HTTPDigestAuth(username, password)

68
httpie/plugins/manager.py Normal file
View File

@ -0,0 +1,68 @@
from itertools import groupby
from pkg_resources import iter_entry_points
from httpie.plugins import AuthPlugin, FormatterPlugin, ConverterPlugin
from httpie.plugins.base import TransportPlugin
ENTRY_POINT_NAMES = [
'httpie.plugins.auth.v1',
'httpie.plugins.formatter.v1',
'httpie.plugins.converter.v1',
'httpie.plugins.transport.v1',
]
class PluginManager(object):
def __init__(self):
self._plugins = []
def __iter__(self):
return iter(self._plugins)
def register(self, *plugins):
for plugin in plugins:
self._plugins.append(plugin)
def unregister(self, plugin):
self._plugins.remove(plugin)
def load_installed_plugins(self):
for entry_point_name in ENTRY_POINT_NAMES:
for entry_point in iter_entry_points(entry_point_name):
plugin = entry_point.load()
plugin.package_name = entry_point.dist.key
self.register(entry_point.load())
# Auth
def get_auth_plugins(self):
return [plugin for plugin in self if issubclass(plugin, AuthPlugin)]
def get_auth_plugin_mapping(self):
return dict((plugin.auth_type, plugin)
for plugin in self.get_auth_plugins())
def get_auth_plugin(self, auth_type):
return self.get_auth_plugin_mapping()[auth_type]
# Output processing
def get_formatters(self):
return [plugin for plugin in self
if issubclass(plugin, FormatterPlugin)]
def get_formatters_grouped(self):
groups = {}
for group_name, group in groupby(
self.get_formatters(),
key=lambda p: getattr(p, 'group_name', 'format')):
groups[group_name] = list(group)
return groups
def get_converters(self):
return [plugin for plugin in self
if issubclass(plugin, ConverterPlugin)]
# Adapters
def get_transport_plugins(self):
return [plugin for plugin in self
if issubclass(plugin, TransportPlugin)]

View File

@ -1,49 +0,0 @@
import os
import json
import pygments
from pygments.util import ClassNotFound
from pygments.styles import get_style_by_name, STYLE_MAP
from pygments.lexers import get_lexer_for_mimetype, HttpLexer
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.formatters.terminal import TerminalFormatter
from . import solarized
DEFAULT_STYLE = 'solarized'
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
FORMATTER = (Terminal256Formatter
if '256color' in os.environ.get('TERM', '')
else TerminalFormatter)
class PrettyHttp(object):
def __init__(self, style_name):
if style_name == 'solarized':
style = solarized.SolarizedStyle
else:
style = get_style_by_name(style_name)
self.formatter = FORMATTER(style=style)
def headers(self, content):
return pygments.highlight(content, HttpLexer(), self.formatter)
def body(self, content, content_type):
content_type = content_type.split(';')[0]
try:
lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
return content
if content_type == 'application/json':
try:
# Indent and sort the JSON data.
content = json.dumps(json.loads(content),
sort_keys=True, indent=4)
except:
pass
return pygments.highlight(content, lexer, self.formatter)

176
httpie/sessions.py Normal file
View File

@ -0,0 +1,176 @@
"""Persistent, JSON-serialized sessions.
"""
import re
import os
from requests.cookies import RequestsCookieJar, create_cookie
from httpie.compat import urlsplit
from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR
from httpie.plugins import plugin_manager
SESSIONS_DIR_NAME = 'sessions'
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
# Request headers starting with these prefixes won't be stored in sessions.
# They are specific to each request.
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(requests_session, session_name,
config_dir, args, read_only=False):
"""Like `client.get_responses`, but applies permanent
aspects of the session to the request.
"""
from .client import get_requests_kwargs, dump_request
if os.path.sep in session_name:
path = os.path.expanduser(session_name)
else:
hostname = (args.headers.get('Host', None) or
urlsplit(args.url).netloc.split('@')[-1])
if not hostname:
# HACK/FIXME: httpie-unixsocket's URLs have no hostname.
hostname = 'localhost'
# host:port => host_port
hostname = hostname.replace(':', '_')
path = os.path.join(config_dir,
SESSIONS_DIR_NAME,
hostname,
session_name + '.json')
session = Session(path)
session.load()
kwargs = get_requests_kwargs(args, base_headers=session.headers)
if args.debug:
dump_request(kwargs)
session.update_headers(kwargs['headers'])
if args.auth_plugin:
session.auth = {
'type': args.auth_plugin.auth_type,
'raw_auth': args.auth_plugin.raw_auth,
}
elif session.auth:
kwargs['auth'] = session.auth
requests_session.cookies = session.cookies
try:
response = requests_session.request(**kwargs)
except Exception:
raise
else:
# Existing sessions with `read_only=True` don't get updated.
if session.is_new() or not read_only:
session.cookies = requests_session.cookies
session.save()
return response
class Session(BaseConfigDict):
helpurl = 'https://httpie.org/docs#sessions'
about = 'HTTPie session file'
def __init__(self, path, *args, **kwargs):
super(Session, self).__init__(*args, **kwargs)
self._path = path
self['headers'] = {}
self['cookies'] = {}
self['auth'] = {
'type': None,
'username': None,
'password': None
}
def _get_path(self):
return self._path
def update_headers(self, request_headers):
"""
Update the session headers with the request ones while ignoring
certain name prefixes.
:type request_headers: dict
"""
for name, value in request_headers.items():
if value is None:
continue # Ignore explicitely unset headers
value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'):
continue
for prefix in SESSION_IGNORED_HEADER_PREFIXES:
if name.lower().startswith(prefix.lower()):
break
else:
self['headers'][name] = value
@property
def headers(self):
return self['headers']
@property
def cookies(self):
jar = RequestsCookieJar()
for name, cookie_dict in self['cookies'].items():
jar.set_cookie(create_cookie(
name, cookie_dict.pop('value'), **cookie_dict))
jar.clear_expired_cookies()
return jar
@cookies.setter
def cookies(self, jar):
"""
:type jar: CookieJar
"""
# http://docs.python.org/2/library/cookielib.html#cookie-objects
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for cookie in jar:
self['cookies'][cookie.name] = dict(
(attname, getattr(cookie, attname))
for attname in stored_attrs
)
@property
def auth(self):
auth = self.get('auth', None)
if not auth or not auth['type']:
return
plugin = plugin_manager.get_auth_plugin(auth['type'])()
credentials = {'username': None, 'password': None}
try:
# New style
plugin.raw_auth = auth['raw_auth']
except KeyError:
# Old style
credentials = {
'username': auth['username'],
'password': auth['password'],
}
else:
if plugin.auth_parse:
from httpie.input import parse_auth
parsed = parse_auth(plugin.raw_auth)
credentials = {
'username': parsed.key,
'password': parsed.value,
}
return plugin.get_auth(**credentials)
@auth.setter
def auth(self, auth):
assert set(['type', 'raw_auth']) == set(auth.keys())
self['auth'] = auth

View File

@ -1,128 +0,0 @@
"""
A Pygments_ style based on the dark background variant of Solarized_.
.. _Pygments: http://pygments.org/
.. _Solarized: http://ethanschoonover.com/solarized
Copyright (c) 2011 Hank Gay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from pygments.style import Style
from pygments.token import (Token, Comment, Name, Keyword, Generic, Number,
Operator, String)
BASE03 = '#002B36'
BASE02 = '#073642'
BASE01 = '#586E75'
BASE00 = '#657B83'
BASE0 = '#839496'
BASE1 = '#93A1A1'
BASE2 = '#EEE8D5'
BASE3 = '#FDF6E3'
YELLOW = '#B58900'
ORANGE = '#CB4B16'
RED = '#DC322F'
MAGENTA = '#D33682'
VIOLET = '#6C71C4'
BLUE = '#268BD2'
CYAN = '#2AA198'
GREEN = '#859900'
class SolarizedStyle(Style):
background_color = BASE03
styles = {
Keyword: GREEN,
Keyword.Constant: ORANGE,
Keyword.Declaration: BLUE,
#Keyword.Namespace
#Keyword.Pseudo
Keyword.Reserved: BLUE,
Keyword.Type: RED,
#Name
Name.Attribute: BASE1,
Name.Builtin: YELLOW,
Name.Builtin.Pseudo: BLUE,
Name.Class: BLUE,
Name.Constant: ORANGE,
Name.Decorator: BLUE,
Name.Entity: ORANGE,
Name.Exception: ORANGE,
Name.Function: BLUE,
#Name.Label
#Name.Namespace
#Name.Other
Name.Tag: BLUE,
Name.Variable: BLUE,
#Name.Variable.Class
#Name.Variable.Global
#Name.Variable.Instance
#Literal
#Literal.Date
String: CYAN,
String.Backtick: BASE01,
String.Char: CYAN,
String.Doc: BASE1,
#String.Double
String.Escape: ORANGE,
String.Heredoc: BASE1,
#String.Interpol
#String.Other
String.Regex: RED,
#String.Single
#String.Symbol
Number: CYAN,
#Number.Float
#Number.Hex
#Number.Integer
#Number.Integer.Long
#Number.Oct
Operator: GREEN,
#Operator.Word
#Punctuation: ORANGE,
Comment: BASE01,
#Comment.Multiline
Comment.Preproc: GREEN,
#Comment.Single
Comment.Special: GREEN,
#Generic
Generic.Deleted: CYAN,
Generic.Emph: 'italic',
Generic.Error: RED,
Generic.Heading: ORANGE,
Generic.Inserted: GREEN,
#Generic.Output
#Generic.Prompt
Generic.Strong: 'bold',
Generic.Subheading: ORANGE,
#Generic.Traceback
Token: BASE1,
Token.Other: ORANGE,
}

72
httpie/utils.py Normal file
View File

@ -0,0 +1,72 @@
from __future__ import division
import json
from httpie.compat import is_py26, OrderedDict
def load_json_preserve_order(s):
if is_py26:
return json.loads(s)
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):
# Author: Doug Latornell
# Licence: MIT
# URL: http://code.activestate.com/recipes/577081/
"""Return a humanized string representation of a number of bytes.
Assumes `from __future__ import division`.
>>> humanize_bytes(1)
'1 B'
>>> humanize_bytes(1024, precision=1)
'1.0 kB'
>>> humanize_bytes(1024 * 123, precision=1)
'123.0 kB'
>>> humanize_bytes(1024 * 12342, precision=1)
'12.1 MB'
>>> humanize_bytes(1024 * 12342, precision=2)
'12.05 MB'
>>> humanize_bytes(1024 * 1234, precision=2)
'1.21 MB'
>>> humanize_bytes(1024 * 1234 * 1111, precision=2)
'1.31 GB'
>>> humanize_bytes(1024 * 1234 * 1111, precision=1)
'1.3 GB'
"""
abbrevs = [
(1 << 50, 'PB'),
(1 << 40, 'TB'),
(1 << 30, 'GB'),
(1 << 20, 'MB'),
(1 << 10, 'kB'),
(1, 'B')
]
if n == 1:
return '1 B'
for factor, suffix in abbrevs:
if n >= factor:
break
# noinspection PyUnboundLocalVariable
return '%.*f %s' % (precision, n / factor, suffix)

2
pytest.ini Normal file
View File

@ -0,0 +1,2 @@
[pytest]
norecursedirs = tests/fixtures

7
requirements-dev.txt Normal file
View File

@ -0,0 +1,7 @@
tox
mock
pytest
pytest-cov
pytest-httpbin>=0.0.6
docutils
wheel

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[wheel]
universal = 1

View File

@ -1,45 +1,106 @@
import os
# This is purely the result of trial and error.
import sys
from setuptools import setup
import codecs
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
import httpie
if sys.argv[-1] == 'test':
os.system('python tests.py')
sys.exit()
class PyTest(TestCommand):
# `$ python setup.py test' simply installs minimal requirements
# and runs the tests with no fancy stuff like parallel execution.
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = [
'--doctest-modules', '--verbose',
'./httpie', './tests'
]
self.test_suite = True
def run_tests(self):
import pytest
sys.exit(pytest.main(self.test_args))
# Debian has only requests==0.10.1 and httpie.deb depends on that.
requirements = ['requests>=0.10.1', 'Pygments>=1.5']
if sys.version_info[:2] in ((2, 6), (3, 1)):
# argparse has been added in Python 3.2 / 2.7
requirements.append('argparse>=1.2.1')
tests_require = [
# Pytest needs to come last.
# https://bitbucket.org/pypa/setuptools/issue/196/
'pytest-httpbin',
'pytest',
'mock',
]
install_requires = [
'requests>=2.11.0',
'Pygments>=2.1.3'
]
# Conditional dependencies:
# sdist
if 'bdist_wheel' not in sys.argv:
try:
# noinspection PyUnresolvedReferences
import argparse
except ImportError:
install_requires.append('argparse>=1.2.1')
if 'win32' in str(sys.platform).lower():
# Terminal colors for Windows
install_requires.append('colorama>=0.2.4')
# bdist_wheel
extras_require = {
# http://wheel.readthedocs.io/en/latest/#defining-conditional-dependencies
':python_version == "2.6"'
' or python_version == "3.0"'
' or python_version == "3.1" ': ['argparse>=1.2.1'],
':sys_platform == "win32"': ['colorama>=0.2.4'],
}
def long_description():
with codecs.open('README.rst', encoding='utf8') as f:
return f.read()
setup(
name='httpie',
version=httpie.__version__,
description=httpie.__doc__.strip(),
long_description=open('README.rst').read(),
long_description=long_description(),
url='http://httpie.org/',
download_url='https://github.com/jkbr/httpie',
download_url='https://github.com/jkbrzt/httpie',
author=httpie.__author__,
author_email='jakub@roztocil.name',
author_email='jakub@roztocil.co',
license=httpie.__licence__,
packages=['httpie'],
packages=find_packages(),
entry_points={
'console_scripts': [
'http = httpie.__main__:main',
],
},
install_requires=requirements,
extras_require=extras_require,
install_requires=install_requires,
tests_require=tests_require,
cmdclass={'test': PyTest},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
@ -49,5 +110,6 @@ setup(
'Topic :: System :: Networking',
'Topic :: Terminals',
'Topic :: Text Processing',
'Topic :: Utilities'
],
)

8
tests/README.rst Normal file
View File

@ -0,0 +1,8 @@
HTTPie Test Suite
=================
Please see `CONTRIBUTING`_.
.. _CONTRIBUTING: https://github.com/jkbrzt/httpie/blob/master/CONTRIBUTING.rst

View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFAjCCAuoCAQEwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCVVMxCzAJBgNV
BAgTAkNBMQswCQYDVQQHEwJTRjEPMA0GA1UEChMGSFRUUGllMQ8wDQYDVQQDEwZI
VFRQaWUwHhcNMTUwMTIzMjIyNTM2WhcNMTYwMTIzMjIyNTM2WjBFMQswCQYDVQQG
EwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lk
Z2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu6aP
iR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/Rn5mCMKmD506JrFV8fktQ
M6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535lb9V9hHjAgy60QgJBgSE7
lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et0RQiWIi7S6vpDRpZFxRi
gtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQI6JadczU0JyVVjJVTny3
ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ2nc+OrJwYLvOp1cG/zYl
GHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK3gEbMz3y+YTlVNPo108H
JI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZdVH3feAhTfDZbpSxhpRo
Ja84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQFTCjN22UhPP0PrqY3ngEj
1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5Vr89NO08QtnLwQduusVkc
4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0EyV2z6pZiH6HK1r5Xwaq
0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEAATANBgkqhkiG9w0BAQUF
AAOCAgEAQgIicN/uWtaYYBVEVeMGMdxzpp2pv3AaCfQMoVGaQu9VLydK/GBlYOqj
AGPjdmQ7p4ISlduXqslu646+RxZ+H6TSSj0NTF4FyR8LPckRPiePNlsGp3u6ffix
PX0554Ks+JYyFJ7qyMhsilqCYtw8prX9lj8fjzbWWXlgJFH/SRZw4xdcJ1yYA9sQ
fBHxveCWFS1ibX5+QGy/+7jPb99MP38HEIt9vTMW5aiwXeIbipXohWqcJhxL9GXz
KPsrt9a++rLjqsquhZL4uCksGmI4Gv0FQQswgSyHSSQzagee5VRB68WYSAyYdvzi
YCfkNcbQtOOQWGx4rsEdENViPs1GEZkWJJ1h9pmWzZl0U9c3cnABffK7o9v6ap2F
NrnU5H/7jLuBiUJFzqwkgAjANLRZ6hLj6h/grcnIIThJwg6KaXvpEh4UkHuqHYBF
Fq1BWZIWU25ASggEVIsCPXC2+I1oGhxK1DN/J+wIht9MBWWlQWVMZAQsBkszNZrh
nzdfMoQZTG5bT4Bf0bI5LmPaY0xBxXA1f4TLuqrEAziOjRX3vIQV4i33nZZJvPcC
mCoyhAUpTJm+OI90ePll+vBO1ENAx7EMHqNe6eCChZ/9DUsVxxtaorVq1l0xWons
ynOCgx46hGE12/oiRIKq/wGMpv6ClfJhW1N5nJahDqoIMEvnNaQ=
-----END CERTIFICATE-----

View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAu6aPiR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/R
n5mCMKmD506JrFV8fktQM6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535l
b9V9hHjAgy60QgJBgSE7lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et
0RQiWIi7S6vpDRpZFxRigtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQ
I6JadczU0JyVVjJVTny3ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ
2nc+OrJwYLvOp1cG/zYlGHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK
3gEbMz3y+YTlVNPo108HJI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZ
dVH3feAhTfDZbpSxhpRoJa84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQF
TCjN22UhPP0PrqY3ngEj1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5V
r89NO08QtnLwQduusVkc4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0
EyV2z6pZiH6HK1r5Xwaq0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEA
AQKCAgBOY1DYlZYg8/eXAhuDDkayYYzDuny1ylG8c4F9nFYVCxB2GZ1Wz3icPWP1
j1BhpkBgPbPeLfM+O0V1H6eCdVvapKOxXM52mDuHO3TJP6P8lOZgZOOY6RUK7qp0
4mC4plqYx7oto23CBLoOdgMtM937rG0SLGDfIF6z8sI0XCMRkqPpRviNu5xxYYTk
IoczSwtmYcSZJRjHhk4AGnmicDbMPRlJ2k2E0euHhI9wMAyQFUFnhLJlQGALj6pj
DtYvcM1EAUN46EXK66bXQq8zgozYS0WIJ6+wOUKQMSIgUGCF6Rvm3ZTt9xwOxxW8
wxebvfYVTJgIdh2Nfusgmye9Debl73f+k9/O4RsvYc5J5w2n4IxKqQrfCZrZqevZ
s+KvARkuQbXrHPanvEd8MPrRZ6FOAdiZYAbB9OvzuKCbEkgag8GPjMMAvrjT49N2
qp9gwGgnzczQYn+vLblJuRzofcblvLE+sxKKDE8qrfcOjN1murZP7714y5E3NmEZ
NB2NTHveTflYI1HJ1tznI1C40GdBYH4GwT/0he53rBcjNaPhyP7j3cTR1doRfZap
2oz8KE/Sij3Zb6b8r7hi+Lcwpa9txZftro7XNOJIX7ZT5B4KMiXowtCHbkMMnL6k
48tRBpyX20MqDFezBRCK7lfGhU1Coms8UcDHoFXLuGY/sAYEcQKCAQEA9D9/PD1t
e90haG6nLl4LKP5wH2wB2BK1RRBERqOVqwSmdSgn3/+GkpeYWKdhN2jyYn6qnpJQ
hXXYGtHAAHuw0dGXnOmgcsyZSlAWPzpMYRYrSh3ds8JVJdV2d58yS0ty3Ae3W6aW
p4SRuhf8yIMgOmE+TARCU1rJdke9jIIl2TQmnpJahlsZeGLEmEXE99EhB5VoshRJ
hLXNn3xTtkQz3tNR0rMAtXI6SIMB00FFEG1+jClza6PYriT9dkORI5LSVqXDEpxR
C41PvYMKTAloWd0hZ2gdfwAcJScoAv75L10uR7O1IeQI+Et5h2tj4a/OfzILa0d5
BYMmVsTa3NZXLQKCAQEAxK3uJKmoN2uswJQSKpX4WziVBgbaZdpCNnAWhfJZYIcP
zlIuv9qOc/DIPiy9Sa9XtITSkcONAwRowtI783AtXeAelyo3W7A2aLIfBBZAXDzJ
8KMc9xMDPebvUhlPSzg4bNwvduukAnktlzCjrRWPXRoSfloSpFkFPP4GwTdVcf17
1mkki6rK4rbHmIoCITlZkNbUBCcu20ivK6N3pvH1wN123bxnF7lwvB5qizdFO5P7
xRVIoCdCXQ0+WK2ZokCa/r44rcp4ffgrLoO/WRlo4yERIa9NwaucIrXmotKX8kYc
YYpFzrGs72DljS7TBZCOqek5fNQBNK79BA2hNcJ1FQKCAQBC+M44hFdq6T1p1z18
F0lUGkBAPWtcBfUyVL2D6QL2+7Vw1mvonbYWp/6cAHlFqj8cBsNd65ysm51/7ReK
il/3iFLcMatPDw7RM5iGCcQ7ssp37iyGR7j1QMzVDA/MWYnLD0qVlN4mXNFgh4dG
q73AhD2CtoBBPtmS1yUATAd4wTX9sP+la4FWYy6o2iiiEvPNkog8nBd0ji0tl/eU
OKtIZAVBkteU6RdWHqX3eSQo1v0mDY+aajjVt0rQjMJVUMLgA1+z0KzgUAUXX8EJ
DGNSkLHCGuhLlIojHdN4ztUgyZoRCxOVkWNsQbW3Dhk7HuuuMNi0t8pVWpq+nAev
Gg6ZAoIBAQC0mMk9nRO7oAGG6/Aqbn8YtEISwKQ2Nk3qUs47vKdZPWvEFi6bOILp
70TP4qEFUh6EwhngguGuzZOsoQMvq+fcdXlhcQBYDtxHEpfsVspOZ/s+HWjxbuHh
K3bBuj/XYA5f12c2GXYGV2MHm0AQJOX5pYEpyGepxZxLvy5QqRCqlQnrfaxzGycl
OpTYepEuFM0rdDhGf/xEmt9OgNHT2AXDTRhizycS39Kmyn8myl+mL2JWPA7uEF6d
txVytCWImS45kE3XNz2g3go4sf04QV7QgIKMnb4Wgg/ix4i6JgokC0DwR9mFzBxx
ylW+aCqYx35YgrGo77sTt0LZP/KxvJdpAoIBAF7YfhR1wFbW2L8sJ4gAbjPUWOMu
JUfE4FhdLcSdqCo+N8uN0qawJxXltBKfjeeoH0CDh9Yv0qqalVdSOKS9BPAa1zJc
o2kBcT8AVwoPS5oxa9eDT+7iHPMF4BErB2IGv3yYwpjqSZBJ9TsTu1B6iTf5hOL5
9pqcv/LjfcwtWu2XMCVoZj2Q8iYv55l3jJ1ByF/UDVezWajE69avvJkQZrMZmuBw
UuHelP/7anRyyelh7RkndZpPCExGmuO7pd5aG25/mBs0i34R1PElAtt8AN36f5Tk
1GxIltTNtLk4Mivwp9aZ1vf9s5FAhgPDvfGV5yFoKYmA/65ZlrKx0zlFNng=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,87 @@
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
issuer=/C=US/ST=CA/L=SF/O=HTTPie/CN=HTTPie
-----BEGIN CERTIFICATE-----
MIIFAjCCAuoCAQEwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UEBhMCVVMxCzAJBgNV
BAgTAkNBMQswCQYDVQQHEwJTRjEPMA0GA1UEChMGSFRUUGllMQ8wDQYDVQQDEwZI
VFRQaWUwHhcNMTUwMTIzMjIyNTM2WhcNMTYwMTIzMjIyNTM2WjBFMQswCQYDVQQG
EwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lk
Z2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu6aP
iR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/Rn5mCMKmD506JrFV8fktQ
M6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535lb9V9hHjAgy60QgJBgSE7
lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et0RQiWIi7S6vpDRpZFxRi
gtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQI6JadczU0JyVVjJVTny3
ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ2nc+OrJwYLvOp1cG/zYl
GHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK3gEbMz3y+YTlVNPo108H
JI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZdVH3feAhTfDZbpSxhpRo
Ja84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQFTCjN22UhPP0PrqY3ngEj
1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5Vr89NO08QtnLwQduusVkc
4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0EyV2z6pZiH6HK1r5Xwaq
0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEAATANBgkqhkiG9w0BAQUF
AAOCAgEAQgIicN/uWtaYYBVEVeMGMdxzpp2pv3AaCfQMoVGaQu9VLydK/GBlYOqj
AGPjdmQ7p4ISlduXqslu646+RxZ+H6TSSj0NTF4FyR8LPckRPiePNlsGp3u6ffix
PX0554Ks+JYyFJ7qyMhsilqCYtw8prX9lj8fjzbWWXlgJFH/SRZw4xdcJ1yYA9sQ
fBHxveCWFS1ibX5+QGy/+7jPb99MP38HEIt9vTMW5aiwXeIbipXohWqcJhxL9GXz
KPsrt9a++rLjqsquhZL4uCksGmI4Gv0FQQswgSyHSSQzagee5VRB68WYSAyYdvzi
YCfkNcbQtOOQWGx4rsEdENViPs1GEZkWJJ1h9pmWzZl0U9c3cnABffK7o9v6ap2F
NrnU5H/7jLuBiUJFzqwkgAjANLRZ6hLj6h/grcnIIThJwg6KaXvpEh4UkHuqHYBF
Fq1BWZIWU25ASggEVIsCPXC2+I1oGhxK1DN/J+wIht9MBWWlQWVMZAQsBkszNZrh
nzdfMoQZTG5bT4Bf0bI5LmPaY0xBxXA1f4TLuqrEAziOjRX3vIQV4i33nZZJvPcC
mCoyhAUpTJm+OI90ePll+vBO1ENAx7EMHqNe6eCChZ/9DUsVxxtaorVq1l0xWons
ynOCgx46hGE12/oiRIKq/wGMpv6ClfJhW1N5nJahDqoIMEvnNaQ=
-----END CERTIFICATE-----
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
Key Attributes: <No Attributes>
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAu6aPiR3TpPESWKTS969fxNRoSxl8P4osjhIaUuwblFNZc8/R
n5mCMKmD506JrFV8fktQM6JRL7QuDC9vCw0ycr2HCV1sYX/ICgPCXYgmyigH535l
b9V9hHjAgy60QgJBgSE7lmMYaPpX6OKbT7UlzSwYtfHomXEBFA18Rlc9GwMXH8Et
0RQiWIi7S6vpDRpZFxRigtXMceK1X8kut2ODv9B5ZwiuXh7+AMSCUkO58bXJTewQ
I6JadczU0JyVVjJVTny3ta0x4SyXn8/ibylOalIsmTd/CAXJRfhV0Umb34LwaWrZ
2nc+OrJwYLvOp1cG/zYlGHkFCViRfuwwSkL4iKjVeHx2o0DxJ4bc2Z7k1ig2fTJK
3gEbMz3y+YTlVNPo108HJI77DPbkBUqLPeF7PMaN/zDqmdH0yNCW+WiHZlf6h7kZ
dVH3feAhTfDZbpSxhpRoJa84OAVCNqAuNjnZs8pMIW/iRixwP8p84At7VsS4yQQF
TCjN22UhPP0PrqY3ngEj1lbfhHC1FNZvCMxrkUAUQbeYRqLrIwB4KdDMkRJixv5V
r89NO08QtnLwQduusVkc4Zg9HXtJTKjgQTHxHtn+OrTbpx0ogaUuYpVcQOsBT3b0
EyV2z6pZiH6HK1r5Xwaq0+nvFwpCHe58PlaI3Geihxejkv+85ZgDqXSGt7ECAwEA
AQKCAgBOY1DYlZYg8/eXAhuDDkayYYzDuny1ylG8c4F9nFYVCxB2GZ1Wz3icPWP1
j1BhpkBgPbPeLfM+O0V1H6eCdVvapKOxXM52mDuHO3TJP6P8lOZgZOOY6RUK7qp0
4mC4plqYx7oto23CBLoOdgMtM937rG0SLGDfIF6z8sI0XCMRkqPpRviNu5xxYYTk
IoczSwtmYcSZJRjHhk4AGnmicDbMPRlJ2k2E0euHhI9wMAyQFUFnhLJlQGALj6pj
DtYvcM1EAUN46EXK66bXQq8zgozYS0WIJ6+wOUKQMSIgUGCF6Rvm3ZTt9xwOxxW8
wxebvfYVTJgIdh2Nfusgmye9Debl73f+k9/O4RsvYc5J5w2n4IxKqQrfCZrZqevZ
s+KvARkuQbXrHPanvEd8MPrRZ6FOAdiZYAbB9OvzuKCbEkgag8GPjMMAvrjT49N2
qp9gwGgnzczQYn+vLblJuRzofcblvLE+sxKKDE8qrfcOjN1murZP7714y5E3NmEZ
NB2NTHveTflYI1HJ1tznI1C40GdBYH4GwT/0he53rBcjNaPhyP7j3cTR1doRfZap
2oz8KE/Sij3Zb6b8r7hi+Lcwpa9txZftro7XNOJIX7ZT5B4KMiXowtCHbkMMnL6k
48tRBpyX20MqDFezBRCK7lfGhU1Coms8UcDHoFXLuGY/sAYEcQKCAQEA9D9/PD1t
e90haG6nLl4LKP5wH2wB2BK1RRBERqOVqwSmdSgn3/+GkpeYWKdhN2jyYn6qnpJQ
hXXYGtHAAHuw0dGXnOmgcsyZSlAWPzpMYRYrSh3ds8JVJdV2d58yS0ty3Ae3W6aW
p4SRuhf8yIMgOmE+TARCU1rJdke9jIIl2TQmnpJahlsZeGLEmEXE99EhB5VoshRJ
hLXNn3xTtkQz3tNR0rMAtXI6SIMB00FFEG1+jClza6PYriT9dkORI5LSVqXDEpxR
C41PvYMKTAloWd0hZ2gdfwAcJScoAv75L10uR7O1IeQI+Et5h2tj4a/OfzILa0d5
BYMmVsTa3NZXLQKCAQEAxK3uJKmoN2uswJQSKpX4WziVBgbaZdpCNnAWhfJZYIcP
zlIuv9qOc/DIPiy9Sa9XtITSkcONAwRowtI783AtXeAelyo3W7A2aLIfBBZAXDzJ
8KMc9xMDPebvUhlPSzg4bNwvduukAnktlzCjrRWPXRoSfloSpFkFPP4GwTdVcf17
1mkki6rK4rbHmIoCITlZkNbUBCcu20ivK6N3pvH1wN123bxnF7lwvB5qizdFO5P7
xRVIoCdCXQ0+WK2ZokCa/r44rcp4ffgrLoO/WRlo4yERIa9NwaucIrXmotKX8kYc
YYpFzrGs72DljS7TBZCOqek5fNQBNK79BA2hNcJ1FQKCAQBC+M44hFdq6T1p1z18
F0lUGkBAPWtcBfUyVL2D6QL2+7Vw1mvonbYWp/6cAHlFqj8cBsNd65ysm51/7ReK
il/3iFLcMatPDw7RM5iGCcQ7ssp37iyGR7j1QMzVDA/MWYnLD0qVlN4mXNFgh4dG
q73AhD2CtoBBPtmS1yUATAd4wTX9sP+la4FWYy6o2iiiEvPNkog8nBd0ji0tl/eU
OKtIZAVBkteU6RdWHqX3eSQo1v0mDY+aajjVt0rQjMJVUMLgA1+z0KzgUAUXX8EJ
DGNSkLHCGuhLlIojHdN4ztUgyZoRCxOVkWNsQbW3Dhk7HuuuMNi0t8pVWpq+nAev
Gg6ZAoIBAQC0mMk9nRO7oAGG6/Aqbn8YtEISwKQ2Nk3qUs47vKdZPWvEFi6bOILp
70TP4qEFUh6EwhngguGuzZOsoQMvq+fcdXlhcQBYDtxHEpfsVspOZ/s+HWjxbuHh
K3bBuj/XYA5f12c2GXYGV2MHm0AQJOX5pYEpyGepxZxLvy5QqRCqlQnrfaxzGycl
OpTYepEuFM0rdDhGf/xEmt9OgNHT2AXDTRhizycS39Kmyn8myl+mL2JWPA7uEF6d
txVytCWImS45kE3XNz2g3go4sf04QV7QgIKMnb4Wgg/ix4i6JgokC0DwR9mFzBxx
ylW+aCqYx35YgrGo77sTt0LZP/KxvJdpAoIBAF7YfhR1wFbW2L8sJ4gAbjPUWOMu
JUfE4FhdLcSdqCo+N8uN0qawJxXltBKfjeeoH0CDh9Yv0qqalVdSOKS9BPAa1zJc
o2kBcT8AVwoPS5oxa9eDT+7iHPMF4BErB2IGv3yYwpjqSZBJ9TsTu1B6iTf5hOL5
9pqcv/LjfcwtWu2XMCVoZj2Q8iYv55l3jJ1ByF/UDVezWajE69avvJkQZrMZmuBw
UuHelP/7anRyyelh7RkndZpPCExGmuO7pd5aG25/mBs0i34R1PElAtt8AN36f5Tk
1GxIltTNtLk4Mivwp9aZ1vf9s5FAhgPDvfGV5yFoKYmA/65ZlrKx0zlFNng=
-----END RSA PRIVATE KEY-----

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

@ -1 +0,0 @@
__test_file_content__

40
tests/fixtures/__init__.py vendored Normal file
View File

@ -0,0 +1,40 @@
"""Test data"""
from os import path
import codecs
def patharg(path):
"""
Back slashes need to be escaped in ITEM args,
even in Windows paths.
"""
return path.replace('\\', '\\\\\\')
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__)))
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt')
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json')
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin')
FILE_PATH_ARG = patharg(FILE_PATH)
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
with codecs.open(FILE_PATH, encoding='utf8') as f:
# Strip because we don't want new lines in the data so that we can
# easily count occurrences also when embedded in JSON (where the new
# line would be escaped).
FILE_CONTENT = f.read().strip()
with codecs.open(JSON_FILE_PATH, encoding='utf8') as f:
JSON_FILE_CONTENT = f.read()
with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read()
UNICODE = FILE_CONTENT

BIN
tests/fixtures/test.bin vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

4
tests/fixtures/test.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"name": "Jakub Roztočil",
"unicode": "χρυσαφὶ 太陽 เลิศ ♜♞♝♛♚♝♞♜ оживлённым तान्यहानि 有朋"
}

1
tests/fixtures/test.txt vendored Normal file
View File

@ -0,0 +1 @@
[one line of UTF8-encoded unicode text] χρυσαφὶ 太陽 เลิศ ♜♞♝♛♚♝♞♜ оживлённым तान्यहानि 有朋 ஸ்றீனிவாஸ ٱلرَّحْمـَبنِ

75
tests/test_auth.py Normal file
View File

@ -0,0 +1,75 @@
"""HTTP authentication-related tests."""
import mock
import pytest
from utils import http, add_auth, HTTP_OK, TestEnvironment
import httpie.input
import httpie.cli
def test_basic_auth(httpbin_both):
r = http('--auth=user:password',
'GET', httpbin_both + '/basic-auth/user/password')
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('argument_name', ['--auth-type', '-A'])
def test_digest_auth(httpbin_both, argument_name):
r = http(argument_name + '=digest', '--auth=user:password',
'GET', httpbin_both.url + '/digest-auth/auth/user/password')
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
@mock.patch('httpie.input.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_password_prompt(httpbin):
r = http('--auth', 'user',
'GET', httpbin.url + '/basic-auth/user/password')
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url(httpbin_both):
url = add_auth(httpbin_both.url + '/basic-auth/user/password',
auth='user:password')
r = http('GET', url)
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
def test_credentials_in_url_auth_flag_has_priority(httpbin_both):
"""When credentials are passed in URL and via -a at the same time,
then the ones from -a are used."""
url = add_auth(httpbin_both.url + '/basic-auth/user/password',
auth='user:wrong')
r = http('--auth=user:password', 'GET', url)
assert HTTP_OK in r
assert r.json == {'authenticated': True, 'user': 'user'}
@pytest.mark.parametrize('url', [
'username@example.org',
'username:@example.org',
])
def test_only_username_in_url(url):
"""
https://github.com/jkbrzt/httpie/issues/242
"""
args = httpie.cli.parser.parse_args(args=[url], env=TestEnvironment())
assert args.auth
assert args.auth.username == 'username'
assert args.auth.password == ''
def test_missing_auth(httpbin):
r = http(
'--auth-type=basic',
'GET',
httpbin + '/basic-auth/user/password',
error_exit_ok=True
)
assert HTTP_OK not in r
assert '--auth required' in r.stderr

133
tests/test_auth_plugins.py Normal file
View File

@ -0,0 +1,133 @@
from mock import mock
from httpie.input import SEP_CREDENTIALS
from httpie.plugins import AuthPlugin, plugin_manager
from utils import http, HTTP_OK
# TODO: run all these tests in session mode as well
USERNAME = 'user'
PASSWORD = 'password'
# Basic auth encoded `USERNAME` and `PASSWORD`
# noinspection SpellCheckingInspection
BASIC_AUTH_HEADER_VALUE = 'Basic dXNlcjpwYXNzd29yZA=='
BASIC_AUTH_URL = '/basic-auth/{0}/{1}'.format(USERNAME, PASSWORD)
AUTH_OK = {'authenticated': True, 'user': USERNAME}
def basic_auth(header=BASIC_AUTH_HEADER_VALUE):
def inner(r):
r.headers['Authorization'] = header
return r
return inner
def test_auth_plugin_parse_auth_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-parse-false'
auth_parse = False
def get_auth(self, username=None, password=None):
assert username is None
assert password is None
assert self.raw_auth == BASIC_AUTH_HEADER_VALUE
return basic_auth(self.raw_auth)
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
BASIC_AUTH_HEADER_VALUE,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
def test_auth_plugin_require_auth_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-require-false'
auth_require = False
def get_auth(self, username=None, password=None):
assert self.raw_auth is None
assert username is None
assert password is None
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
def test_auth_plugin_require_auth_false_and_auth_provided(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-require-false-yet-provided'
auth_require = False
def get_auth(self, username=None, password=None):
assert self.raw_auth == USERNAME + SEP_CREDENTIALS + PASSWORD
assert username == USERNAME
assert password == PASSWORD
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
USERNAME + SEP_CREDENTIALS + PASSWORD,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)
@mock.patch('httpie.input.AuthCredentials._getpass',
new=lambda self, prompt: 'UNEXPECTED_PROMPT_RESPONSE')
def test_auth_plugin_prompt_password_false(httpbin):
class Plugin(AuthPlugin):
auth_type = 'test-prompt-false'
prompt_password = False
def get_auth(self, username=None, password=None):
assert self.raw_auth == USERNAME
assert username == USERNAME
assert password is None
return basic_auth()
plugin_manager.register(Plugin)
try:
r = http(
httpbin + BASIC_AUTH_URL,
'--auth-type',
Plugin.auth_type,
'--auth',
USERNAME,
)
assert HTTP_OK in r
assert r.json == AUTH_OK
finally:
plugin_manager.unregister(Plugin)

55
tests/test_binary.py Normal file
View File

@ -0,0 +1,55 @@
"""Tests for dealing with binary request and response data."""
from fixtures import BIN_FILE_PATH, BIN_FILE_CONTENT, BIN_FILE_PATH_ARG
from httpie.compat import urlopen
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import TestEnvironment, http
class TestBinaryRequestData:
def test_binary_stdin(self, httpbin):
with open(BIN_FILE_PATH, 'rb') as stdin:
env = TestEnvironment(
stdin=stdin,
stdin_isatty=False,
stdout_isatty=False
)
r = http('--print=B', 'POST', httpbin.url + '/post', env=env)
assert r == BIN_FILE_CONTENT
def test_binary_file_path(self, httpbin):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=B', 'POST', httpbin.url + '/post',
'@' + BIN_FILE_PATH_ARG, env=env, )
assert r == BIN_FILE_CONTENT
def test_binary_file_form(self, httpbin):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=B', '--form', 'POST', httpbin.url + '/post',
'test@' + BIN_FILE_PATH_ARG, env=env)
assert bytes(BIN_FILE_CONTENT) in bytes(r)
class TestBinaryResponseData:
url = 'http://www.google.com/favicon.ico'
@property
def bindata(self):
if not hasattr(self, '_bindata'):
self._bindata = urlopen(self.url).read()
return self._bindata
def test_binary_suppresses_when_terminal(self):
r = http('GET', self.url)
assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_suppresses_when_not_terminal_but_pretty(self):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--pretty=all', 'GET', self.url,
env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_binary_included_and_correct_when_suitable(self):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('GET', self.url, env=env)
assert r == self.bindata

347
tests/test_cli.py Normal file
View File

@ -0,0 +1,347 @@
"""CLI argument parsing related tests."""
import json
# noinspection PyCompatibility
import argparse
import pytest
from requests.exceptions import InvalidSchema
from httpie import input
from httpie.input import KeyValue, KeyValueArgType, DataDict
from httpie import ExitStatus
from httpie.cli import parser
from utils import TestEnvironment, http, HTTP_OK
from fixtures import (
FILE_PATH_ARG, JSON_FILE_PATH_ARG,
JSON_FILE_CONTENT, FILE_CONTENT, FILE_PATH
)
class TestItemParsing:
key_value = KeyValueArgType(*input.SEP_GROUP_ALL_ITEMS)
def test_invalid_items(self):
items = ['no-separator']
for item in items:
pytest.raises(argparse.ArgumentTypeError, self.key_value, item)
def test_escape_separator(self):
items = input.parse_items([
# headers
self.key_value(r'foo\:bar:baz'),
self.key_value(r'jack\@jill:hill'),
# data
self.key_value(r'baz\=bar=foo'),
# files
self.key_value(r'bar\@baz@%s' % FILE_PATH_ARG),
])
# `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(items.headers._store.values())
assert headers == {
'foo:bar': 'baz',
'jack@jill': 'hill',
}
assert items.data == {'baz=bar': 'foo'}
assert 'bar@baz' in items.files
@pytest.mark.parametrize(('string', 'key', 'sep', 'value'), [
('path=c:\windows', 'path', '=', 'c:\windows'),
('path=c:\windows\\', 'path', '=', 'c:\windows\\'),
('path\==c:\windows', 'path=', '=', 'c:\windows'),
])
def test_backslash_before_non_special_character_does_not_escape(
self, string, key, sep, value):
expected = KeyValue(orig=string, key=key, sep=sep, value=value)
actual = self.key_value(string)
assert actual == expected
def test_escape_longsep(self):
items = input.parse_items([
self.key_value(r'bob\:==foo'),
])
assert items.params == {'bob:': 'foo'}
def test_valid_items(self):
items = input.parse_items([
self.key_value('string=value'),
self.key_value('Header:value'),
self.key_value('Unset-Header:'),
self.key_value('Empty-Header;'),
self.key_value('list:=["a", 1, {}, false]'),
self.key_value('obj:={"a": "b"}'),
self.key_value('ed='),
self.key_value('bool:=true'),
self.key_value('file@' + FILE_PATH_ARG),
self.key_value('query==value'),
self.key_value('string-embed=@' + FILE_PATH_ARG),
self.key_value('raw-json-embed:=@' + JSON_FILE_PATH_ARG),
])
# Parsed headers
# `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(items.headers._store.values())
assert headers == {
'Header': 'value',
'Unset-Header': None,
'Empty-Header': ''
}
# Parsed data
raw_json_embed = items.data.pop('raw-json-embed')
assert raw_json_embed == json.loads(JSON_FILE_CONTENT)
items.data['string-embed'] = items.data['string-embed'].strip()
assert dict(items.data) == {
"ed": "",
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"},
"string-embed": FILE_CONTENT,
}
# Parsed query string parameters
assert items.params == {'query': 'value'}
# Parsed file fields
assert 'file' in items.files
assert (items.files['file'][1].read().strip().
decode('utf8') == FILE_CONTENT)
def test_multiple_file_fields_with_same_field_name(self):
items = input.parse_items([
self.key_value('file_field@' + FILE_PATH_ARG),
self.key_value('file_field@' + FILE_PATH_ARG),
])
assert len(items.files['file_field']) == 2
def test_multiple_text_fields_with_same_field_name(self):
items = input.parse_items(
[self.key_value('text_field=a'),
self.key_value('text_field=b')],
data_class=DataDict
)
assert items.data['text_field'] == ['a', 'b']
assert list(items.data.items()) == [
('text_field', 'a'),
('text_field', 'b'),
]
class TestQuerystring:
def test_query_string_params_in_url(self, httpbin):
r = http('--print=Hhb', 'GET', httpbin.url + '/get?a=1&b=2')
path = '/get?a=1&b=2'
url = httpbin.url + path
assert HTTP_OK in r
assert 'GET %s HTTP/1.1' % path in r
assert '"url": "%s"' % url in r
def test_query_string_params_items(self, httpbin):
r = http('--print=Hhb', 'GET', httpbin.url + '/get', 'a==1')
path = '/get?a=1'
url = httpbin.url + path
assert HTTP_OK in r
assert 'GET %s HTTP/1.1' % path in r
assert '"url": "%s"' % url in r
def test_query_string_params_in_url_and_items_with_duplicates(self,
httpbin):
r = http('--print=Hhb', 'GET',
httpbin.url + '/get?a=1&a=1', 'a==1', 'a==1')
path = '/get?a=1&a=1&a=1&a=1'
url = httpbin.url + path
assert HTTP_OK in r
assert 'GET %s HTTP/1.1' % path in r
assert '"url": "%s"' % url in r
class TestLocalhostShorthand:
def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=TestEnvironment())
assert args.url == 'http://localhost'
def test_expand_localhost_shorthand_with_slash(self):
args = parser.parse_args(args=[':/'], env=TestEnvironment())
assert args.url == 'http://localhost/'
def test_expand_localhost_shorthand_with_port(self):
args = parser.parse_args(args=[':3000'], env=TestEnvironment())
assert args.url == 'http://localhost:3000'
def test_expand_localhost_shorthand_with_path(self):
args = parser.parse_args(args=[':/path'], env=TestEnvironment())
assert args.url == 'http://localhost/path'
def test_expand_localhost_shorthand_with_port_and_slash(self):
args = parser.parse_args(args=[':3000/'], env=TestEnvironment())
assert args.url == 'http://localhost:3000/'
def test_expand_localhost_shorthand_with_port_and_path(self):
args = parser.parse_args(args=[':3000/path'], env=TestEnvironment())
assert args.url == 'http://localhost:3000/path'
def test_dont_expand_shorthand_ipv6_as_shorthand(self):
args = parser.parse_args(args=['::1'], env=TestEnvironment())
assert args.url == 'http://::1'
def test_dont_expand_longer_ipv6_as_shorthand(self):
args = parser.parse_args(
args=['::ffff:c000:0280'],
env=TestEnvironment()
)
assert args.url == 'http://::ffff:c000:0280'
def test_dont_expand_full_ipv6_as_shorthand(self):
args = parser.parse_args(
args=['0000:0000:0000:0000:0000:0000:0000:0001'],
env=TestEnvironment()
)
assert args.url == 'http://0000:0000:0000:0000:0000:0000:0000:0001'
class TestArgumentParser:
def setup_method(self, method):
self.parser = input.HTTPieArgumentParser()
def test_guess_when_method_set_and_valid(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = 'GET'
self.parser.args.url = 'http://example.com/'
self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment()
self.parser._guess_method()
assert self.parser.args.method == 'GET'
assert self.parser.args.url == 'http://example.com/'
assert self.parser.args.items == []
def test_guess_when_method_not_set(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = None
self.parser.args.url = 'http://example.com/'
self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment()
self.parser._guess_method()
assert self.parser.args.method == 'GET'
assert self.parser.args.url == 'http://example.com/'
assert self.parser.args.items == []
def test_guess_when_method_set_but_invalid_and_data_field(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = 'http://example.com/'
self.parser.args.url = 'data=field'
self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment()
self.parser._guess_method()
assert self.parser.args.method == 'POST'
assert self.parser.args.url == 'http://example.com/'
assert self.parser.args.items == [
KeyValue(key='data',
value='field',
sep='=',
orig='data=field')
]
def test_guess_when_method_set_but_invalid_and_header_field(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = 'http://example.com/'
self.parser.args.url = 'test:header'
self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment()
self.parser._guess_method()
assert self.parser.args.method == 'GET'
assert self.parser.args.url == 'http://example.com/'
assert self.parser.args.items, [
KeyValue(key='test',
value='header',
sep=':',
orig='test:header')
]
def test_guess_when_method_set_but_invalid_and_item_exists(self):
self.parser.args = argparse.Namespace()
self.parser.args.method = 'http://example.com/'
self.parser.args.url = 'new_item=a'
self.parser.args.items = [
KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b')
]
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment()
self.parser._guess_method()
assert self.parser.args.items, [
KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b'),
]
class TestNoOptions:
def test_valid_no_options(self, httpbin):
r = http('--verbose', '--no-verbose', 'GET', httpbin.url + '/get')
assert 'GET /get HTTP/1.1' not in r
def test_invalid_no_options(self, httpbin):
r = http('--no-war', 'GET', httpbin.url + '/get',
error_exit_ok=True)
assert r.exit_status == 1
assert 'unrecognized arguments: --no-war' in r.stderr
assert 'GET /get HTTP/1.1' not in r
class TestIgnoreStdin:
def test_ignore_stdin(self, httpbin):
with open(FILE_PATH) as f:
env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get',
env=env)
assert HTTP_OK in r
assert 'GET /get HTTP' in r, "Don't default to POST."
assert FILE_CONTENT not in r, "Don't send stdin data."
def test_ignore_stdin_cannot_prompt_password(self, httpbin):
r = http('--ignore-stdin', '--auth=no-password', httpbin.url + '/get',
error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR
assert 'because --ignore-stdin' in r.stderr
class TestSchemes:
def test_invalid_custom_scheme(self):
# InvalidSchema is expected because HTTPie
# shouldn't touch a formally valid scheme.
with pytest.raises(InvalidSchema):
http('foo+bar-BAZ.123://bah')
def test_invalid_scheme_via_via_default_scheme(self):
# InvalidSchema is expected because HTTPie
# shouldn't touch a formally valid scheme.
with pytest.raises(InvalidSchema):
http('bah', '--default=scheme=foo+bar-BAZ.123')
def test_default_scheme(self, httpbin_secure):
url = '{0}:{1}'.format(httpbin_secure.host, httpbin_secure.port)
assert HTTP_OK in http(url, '--default-scheme=https')

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']

107
tests/test_defaults.py Normal file
View File

@ -0,0 +1,107 @@
"""
Tests for the provided defaults regarding HTTP method, and --json vs. --form.
"""
from httpie.client import JSON_ACCEPT
from utils import TestEnvironment, http, HTTP_OK
from fixtures import FILE_PATH
class TestImplicitHTTPMethod:
def test_implicit_GET(self, httpbin):
r = http(httpbin.url + '/get')
assert HTTP_OK in r
def test_implicit_GET_with_headers(self, httpbin):
r = http(httpbin.url + '/headers', 'Foo:bar')
assert HTTP_OK in r
assert r.json['headers']['Foo'] == 'bar'
def test_implicit_POST_json(self, httpbin):
r = http(httpbin.url + '/post', 'hello=world')
assert HTTP_OK in r
assert r.json['json'] == {'hello': 'world'}
def test_implicit_POST_form(self, httpbin):
r = http('--form', httpbin.url + '/post', 'foo=bar')
assert HTTP_OK in r
assert r.json['form'] == {'foo': 'bar'}
def test_implicit_POST_stdin(self, httpbin):
with open(FILE_PATH) as f:
env = TestEnvironment(stdin_isatty=False, stdin=f)
r = http('--form', httpbin.url + '/post', env=env)
assert HTTP_OK in r
class TestAutoContentTypeAndAcceptHeaders:
"""
Test that Accept and Content-Type correctly defaults to JSON,
but can still be overridden. The same with Content-Type when --form
-f is used.
"""
def test_GET_no_data_no_auto_headers(self, httpbin):
# https://github.com/jkbrzt/httpie/issues/62
r = http('GET', httpbin.url + '/headers')
assert HTTP_OK in r
assert r.json['headers']['Accept'] == '*/*'
assert 'Content-Type' not in r.json['headers']
def test_POST_no_data_no_auto_headers(self, httpbin):
# JSON headers shouldn't be automatically set for POST with no data.
r = http('POST', httpbin.url + '/post')
assert HTTP_OK in r
assert '"Accept": "*/*"' in r
assert '"Content-Type": "application/json' not in r
def test_POST_with_data_auto_JSON_headers(self, httpbin):
r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT
assert r.json['headers']['Content-Type'] == 'application/json'
def test_GET_with_data_auto_JSON_headers(self, httpbin):
# JSON headers should automatically be set also for GET with data.
r = http('POST', httpbin.url + '/post', 'a=b')
assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT
assert r.json['headers']['Content-Type'] == 'application/json'
def test_POST_explicit_JSON_auto_JSON_accept(self, httpbin):
r = http('--json', 'POST', httpbin.url + '/post')
assert HTTP_OK in r
assert r.json['headers']['Accept'] == JSON_ACCEPT
# Make sure Content-Type gets set even with no data.
# https://github.com/jkbrzt/httpie/issues/137
assert 'application/json' in r.json['headers']['Content-Type']
def test_GET_explicit_JSON_explicit_headers(self, httpbin):
r = http('--json', 'GET', httpbin.url + '/headers',
'Accept:application/xml',
'Content-Type:application/xml')
assert HTTP_OK in r
assert '"Accept": "application/xml"' in r
assert '"Content-Type": "application/xml"' in r
def test_POST_form_auto_Content_Type(self, httpbin):
r = http('--form', 'POST', httpbin.url + '/post')
assert HTTP_OK in r
assert '"Content-Type": "application/x-www-form-urlencoded' in r
def test_POST_form_Content_Type_override(self, httpbin):
r = http('--form', 'POST', httpbin.url + '/post',
'Content-Type:application/xml')
assert HTTP_OK in r
assert '"Content-Type": "application/xml"' in r
def test_print_only_body_when_stdout_redirected_by_default(self, httpbin):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('GET', httpbin.url + '/get', env=env)
assert 'HTTP/' not in r
def test_print_overridable_when_stdout_redirected(self, httpbin):
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--print=h', 'GET', httpbin.url + '/get', env=env)
assert HTTP_OK in r

39
tests/test_docs.py Normal file
View File

@ -0,0 +1,39 @@
import os
import fnmatch
import subprocess
import pytest
from utils import TESTS_ROOT
def has_docutils():
try:
# noinspection PyUnresolvedReferences
import docutils
return True
except ImportError:
return False
def rst_filenames():
for root, dirnames, filenames in os.walk(os.path.dirname(TESTS_ROOT)):
if '.tox' not in root:
for filename in fnmatch.filter(filenames, '*.rst'):
yield os.path.join(root, filename)
filenames = list(rst_filenames())
assert filenames
@pytest.mark.skipif(not has_docutils(), reason='docutils not installed')
@pytest.mark.parametrize('filename', filenames)
def test_rst_file_syntax(filename):
p = subprocess.Popen(
['rst2pseudoxml.py', '--report=1', '--exit-status=1', filename],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE
)
err = p.communicate()[1]
assert p.returncode == 0, err.decode('utf8')

165
tests/test_downloads.py Normal file
View File

@ -0,0 +1,165 @@
import os
import time
import pytest
import mock
from requests.structures import CaseInsensitiveDict
from httpie.compat import urlopen
from httpie.downloads import (
parse_content_range, filename_from_content_disposition, filename_from_url,
get_unique_filename, ContentRangeError, Downloader,
)
from utils import http, TestEnvironment
class Response(object):
# noinspection PyDefaultArgument
def __init__(self, url, headers={}, status_code=200):
self.url = url
self.headers = CaseInsensitiveDict(headers)
self.status_code = status_code
class TestDownloadUtils:
def test_Content_Range_parsing(self):
parse = parse_content_range
assert parse('bytes 100-199/200', 100) == 200
assert parse('bytes 100-199/*', 100) == 200
# missing
pytest.raises(ContentRangeError, parse, None, 100)
# syntax error
pytest.raises(ContentRangeError, parse, 'beers 100-199/*', 100)
# unexpected range
pytest.raises(ContentRangeError, parse, 'bytes 100-199/*', 99)
# invalid instance-length
pytest.raises(ContentRangeError, parse, 'bytes 100-199/199', 100)
# invalid byte-range-resp-spec
pytest.raises(ContentRangeError, parse, 'bytes 100-99/199', 100)
# invalid byte-range-resp-spec
pytest.raises(ContentRangeError, parse, 'bytes 100-100/*', 100)
@pytest.mark.parametrize('header, expected_filename', [
('attachment; filename=hello-WORLD_123.txt', 'hello-WORLD_123.txt'),
('attachment; filename=".hello-WORLD_123.txt"', 'hello-WORLD_123.txt'),
('attachment; filename="white space.txt"', 'white space.txt'),
(r'attachment; filename="\"quotes\".txt"', '"quotes".txt'),
('attachment; filename=/etc/hosts', 'hosts'),
('attachment; filename=', None)
])
def test_Content_Disposition_parsing(self, header, expected_filename):
assert filename_from_content_disposition(header) == expected_filename
def test_filename_from_url(self):
assert 'foo.txt' == filename_from_url(
url='http://example.org/foo',
content_type='text/plain'
)
assert 'foo.html' == filename_from_url(
url='http://example.org/foo',
content_type='text/html; charset=utf8'
)
assert 'foo' == filename_from_url(
url='http://example.org/foo',
content_type=None
)
assert 'foo' == filename_from_url(
url='http://example.org/foo',
content_type='x-foo/bar'
)
@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):
# noinspection PyUnresolvedReferences,PyUnusedLocal
def exists(filename):
if exists.attempt == unique_on_attempt:
return False
exists.attempt += 1
return True
exists.attempt = 0
return exists
get_filename_max_length.return_value = 10
actual = get_unique_filename(orig_name, attempts(unique_on_attempt))
assert expected == actual
class TestDownloads:
# TODO: more tests
def test_actual_download(self, httpbin_both, httpbin):
robots_txt = '/robots.txt'
body = urlopen(httpbin + robots_txt).read().decode()
env = TestEnvironment(stdin_isatty=True, stdout_isatty=False)
r = http('--download', httpbin_both.url + robots_txt, env=env)
assert 'Downloading' in r.stderr
assert '[K' in r.stderr
assert 'Done' in r.stderr
assert body == r
def test_download_with_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response(
url=httpbin_both.url + '/',
headers={'Content-Length': 10}
))
time.sleep(1.1)
downloader.chunk_downloaded(b'12345')
time.sleep(1.1)
downloader.chunk_downloaded(b'12345')
downloader.finish()
assert not downloader.interrupted
def test_download_no_Content_Length(self, httpbin_both):
devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response(url=httpbin_both.url + '/'))
time.sleep(1.1)
downloader.chunk_downloaded(b'12345')
downloader.finish()
assert not downloader.interrupted
def test_download_interrupted(self, httpbin_both):
devnull = open(os.devnull, 'w')
downloader = Downloader(output_file=devnull, progress_file=devnull)
downloader.start(Response(
url=httpbin_both.url + '/',
headers={'Content-Length': 5}
))
downloader.chunk_downloaded(b'1234')
downloader.finish()
assert downloader.interrupted

49
tests/test_errors.py Normal file
View File

@ -0,0 +1,49 @@
import mock
from pytest import raises
from requests import Request, Timeout
from requests.exceptions import ConnectionError
from httpie import ExitStatus
from httpie.core import main
error_msg = None
@mock.patch('httpie.core.get_response')
def test_error(get_response):
def error(msg, *args, **kwargs):
global error_msg
error_msg = msg % args
exc = ConnectionError('Connection aborted')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == ExitStatus.ERROR
assert error_msg == (
'ConnectionError: '
'Connection aborted while doing GET request to URL: '
'http://www.google.com')
@mock.patch('httpie.core.get_response')
def test_error_traceback(get_response):
exc = ConnectionError('Connection aborted')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
with raises(ConnectionError):
main(['--ignore-stdin', '--traceback', 'www.google.com'])
@mock.patch('httpie.core.get_response')
def test_timeout(get_response):
def error(msg, *args, **kwargs):
global error_msg
error_msg = msg % args
exc = Timeout('Request timed out')
exc.request = Request(method='GET', url='http://www.google.com')
get_response.side_effect = exc
ret = main(['--ignore-stdin', 'www.google.com'], custom_log_error=error)
assert ret == ExitStatus.ERROR_TIMEOUT
assert error_msg == 'Request timed out (30s).'

74
tests/test_exit_status.py Normal file
View File

@ -0,0 +1,74 @@
import mock
from httpie import ExitStatus
from utils import TestEnvironment, http, HTTP_OK
def test_keyboard_interrupt_during_arg_parsing_exit_status(httpbin):
with mock.patch('httpie.cli.parser.parse_args',
side_effect=KeyboardInterrupt()):
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_CTRL_C
def test_keyboard_interrupt_in_program_exit_status(httpbin):
with mock.patch('httpie.core.program',
side_effect=KeyboardInterrupt()):
r = http('GET', httpbin.url + '/get', error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_CTRL_C
def test_ok_response_exits_0(httpbin):
r = http('GET', httpbin.url + '/get')
assert HTTP_OK in r
assert r.exit_status == ExitStatus.OK
def test_error_response_exits_0_without_check_status(httpbin):
r = http('GET', httpbin.url + '/status/500')
assert '500 INTERNAL SERVER ERRO' in r
assert r.exit_status == ExitStatus.OK
assert not r.stderr
def test_timeout_exit_status(httpbin):
r = http('--timeout=0.01', 'GET', httpbin.url + '/delay/0.02',
error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR_TIMEOUT
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(
httpbin):
env = TestEnvironment(stdout_isatty=False)
r = http('--check-status', '--headers',
'GET', httpbin.url + '/status/301',
env=env, error_exit_ok=True)
assert '301 MOVED PERMANENTLY' in r
assert r.exit_status == ExitStatus.ERROR_HTTP_3XX
assert '301 moved permanently' in r.stderr.lower()
def test_3xx_check_status_redirects_allowed_exits_0(httpbin):
r = http('--check-status', '--follow',
'GET', httpbin.url + '/status/301',
error_exit_ok=True)
# The redirect will be followed so 200 is expected.
assert HTTP_OK in r
assert r.exit_status == ExitStatus.OK
def test_4xx_check_status_exits_4(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/401',
error_exit_ok=True)
assert '401 UNAUTHORIZED' in r
assert r.exit_status == ExitStatus.ERROR_HTTP_4XX
# Also stderr should be empty since stdout isn't redirected.
assert not r.stderr
def test_5xx_check_status_exits_5(httpbin):
r = http('--check-status', 'GET', httpbin.url + '/status/500',
error_exit_ok=True)
assert '500 INTERNAL SERVER ERROR' in r
assert r.exit_status == ExitStatus.ERROR_HTTP_5XX

119
tests/test_httpie.py Normal file
View File

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

173
tests/test_output.py Normal file
View File

@ -0,0 +1,173 @@
import os
from tempfile import gettempdir
import pytest
from utils import TestEnvironment, http, HTTP_OK, COLOR, CRLF
from httpie import ExitStatus
from httpie.compat import urlopen
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:
def test_verbose(self, httpbin):
r = http('--verbose',
'GET', httpbin.url + '/get', 'test-header:__test__')
assert HTTP_OK in r
assert r.count('__test__') == 2
def test_verbose_form(self, httpbin):
# https://github.com/jkbrzt/httpie/issues/53
r = http('--verbose', '--form', 'POST', httpbin.url + '/post',
'A=B', 'C=D')
assert HTTP_OK in r
assert 'A=B&C=D' in r
def test_verbose_json(self, httpbin):
r = http('--verbose',
'POST', httpbin.url + '/post', 'foo=bar', 'baz=bar')
assert HTTP_OK 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:
@pytest.mark.parametrize(
argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
argvalues=[
('application/json', False, None, 'JSON'),
('application/json+foo', False, None, 'JSON'),
('application/foo+json', False, None, 'JSON'),
('application/json-foo', False, None, 'JSON'),
('application/x-json', False, None, 'JSON'),
('foo/json', False, None, 'JSON'),
('foo/json+bar', False, None, 'JSON'),
('foo/bar+json', False, None, 'JSON'),
('foo/json-foo', False, None, 'JSON'),
('foo/x-json', False, None, 'JSON'),
('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.name == expected_lexer_name
def test_get_lexer_not_found(self):
assert get_lexer('xxx/yyy') is None
class TestPrettyOptions:
"""Test the --pretty flag handling."""
def test_pretty_enabled_by_default(self, httpbin):
env = TestEnvironment(colors=256)
r = http('GET', httpbin.url + '/get', env=env)
assert COLOR in r
def test_pretty_enabled_by_default_unless_stdout_redirected(self, httpbin):
r = http('GET', httpbin.url + '/get')
assert COLOR not in r
def test_force_pretty(self, httpbin):
env = TestEnvironment(stdout_isatty=False, colors=256)
r = http('--pretty=all', 'GET', httpbin.url + '/get', env=env, )
assert COLOR in r
def test_force_ugly(self, httpbin):
r = http('--pretty=none', 'GET', httpbin.url + '/get')
assert COLOR not in r
def test_subtype_based_pygments_lexer_match(self, httpbin):
"""Test that media subtype is used if type/subtype doesn't
match any lexer.
"""
env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=all', httpbin.url + '/post',
'Content-Type:text/foo+json', 'a=b', env=env)
assert COLOR in r
def test_colors_option(self, httpbin):
env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=colors',
'GET', httpbin.url + '/get', 'a=b',
env=env)
# Tests that the JSON data isn't formatted.
assert not r.strip().count('\n')
assert COLOR in r
def test_format_option(self, httpbin):
env = TestEnvironment(colors=256)
r = http('--print=B', '--pretty=format',
'GET', httpbin.url + '/get', 'a=b',
env=env)
# Tests that the JSON data is formatted.
assert r.strip().count('\n') == 2
assert COLOR not in r
class TestLineEndings:
"""
Test that CRLF is properly used in headers
and as the headers/body separator.
"""
def _validate_crlf(self, msg):
lines = iter(msg.splitlines(True))
for header in lines:
if header == CRLF:
break
assert header.endswith(CRLF), repr(header)
else:
assert 0, 'CRLF between headers and body not found in %r' % msg
body = ''.join(lines)
assert CRLF not in body
return body
def test_CRLF_headers_only(self, httpbin):
r = http('--headers', 'GET', httpbin.url + '/get')
body = self._validate_crlf(r)
assert not body, 'Garbage after headers: %r' % r
def test_CRLF_ugly_response(self, httpbin):
r = http('--pretty=none', 'GET', httpbin.url + '/get')
self._validate_crlf(r)
def test_CRLF_formatted_response(self, httpbin):
r = http('--pretty=format', 'GET', httpbin.url + '/get')
assert r.exit_status == ExitStatus.OK
self._validate_crlf(r)
def test_CRLF_ugly_request(self, httpbin):
r = http('--pretty=none', '--print=HB', 'GET', httpbin.url + '/get')
self._validate_crlf(r)
def test_CRLF_formatted_request(self, httpbin):
r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get')
self._validate_crlf(r)

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

27
tests/test_regressions.py Normal file
View File

@ -0,0 +1,27 @@
"""Miscellaneous regression tests"""
import pytest
from utils import http, HTTP_OK
from httpie.compat import is_windows
def test_Host_header_overwrite(httpbin):
"""
https://github.com/jkbrzt/httpie/issues/235
"""
host = 'httpbin.org'
url = httpbin.url + '/get'
r = http('--print=hH', url, 'host:{0}'.format(host))
assert HTTP_OK in r
assert r.lower().count('host:') == 1
assert 'host: {0}'.format(host) in r
@pytest.mark.skipif(is_windows, reason='Unix-only')
def test_output_devnull(httpbin):
"""
https://github.com/jkbrzt/httpie/issues/252
"""
http('--output=/dev/null', httpbin + '/get')

187
tests/test_sessions.py Normal file
View File

@ -0,0 +1,187 @@
# coding=utf-8
import os
import shutil
import sys
from tempfile import gettempdir
import pytest
from httpie.plugins.builtin import HTTPBasicAuth
from utils import TestEnvironment, mk_config_dir, http, HTTP_OK
from fixtures import UNICODE
class SessionTestBase(object):
def start_session(self, httpbin):
"""Create and reuse a unique config dir for each test."""
self.config_dir = mk_config_dir()
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
def env(self):
"""
Return an environment.
Each environment created withing a test method
will share the same config_dir. It is necessary
for session files being reused.
"""
return TestEnvironment(config_dir=self.config_dir)
class TestSessionFlow(SessionTestBase):
"""
These tests start with an existing session created in `setup_method()`.
"""
def start_session(self, httpbin):
"""
Start a full-blown session with a custom request header,
authorization, and response cookies.
"""
super(TestSessionFlow, self).start_session(httpbin)
r1 = http('--follow', '--session=test', '--auth=username:password',
'GET', httpbin.url + '/cookies/set?hello=world',
'Hello:World',
env=self.env())
assert HTTP_OK in r1
def test_session_created_and_reused(self, httpbin):
self.start_session(httpbin)
# Verify that the session created in setup_method() has been used.
r2 = http('--session=test',
'GET', httpbin.url + '/get', env=self.env())
assert HTTP_OK in r2
assert r2.json['headers']['Hello'] == 'World'
assert r2.json['headers']['Cookie'] == 'hello=world'
assert 'Basic ' in r2.json['headers']['Authorization']
def test_session_update(self, httpbin):
self.start_session(httpbin)
# Get a response to a request from the original session.
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r2
# Make a request modifying the session data.
r3 = http('--follow', '--session=test', '--auth=username:password2',
'GET', httpbin.url + '/cookies/set?hello=world2',
'Hello:World2',
env=self.env())
assert HTTP_OK in r3
# Get a response to a request from the updated session.
r4 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r4
assert r4.json['headers']['Hello'] == 'World2'
assert r4.json['headers']['Cookie'] == 'hello=world2'
assert (r2.json['headers']['Authorization'] !=
r4.json['headers']['Authorization'])
def test_session_read_only(self, httpbin):
self.start_session(httpbin)
# Get a response from the original session.
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r2
# Make a request modifying the session data but
# with --session-read-only.
r3 = http('--follow', '--session-read-only=test',
'--auth=username:password2', 'GET',
httpbin.url + '/cookies/set?hello=world2', 'Hello:World2',
env=self.env())
assert HTTP_OK in r3
# Get a response from the updated session.
r4 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r4
# Origin can differ on Travis.
del r2.json['origin'], r4.json['origin']
# Different for each request.
# Should be the same as before r3.
assert r2.json == r4.json
class TestSession(SessionTestBase):
"""Stand-alone session tests."""
def test_session_ignored_header_prefixes(self, httpbin):
self.start_session(httpbin)
r1 = http('--session=test', 'GET', httpbin.url + '/get',
'Content-Type: text/plain',
'If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT',
env=self.env())
assert HTTP_OK in r1
r2 = http('--session=test', 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r2
assert 'Content-Type' not in r2.json['headers']
assert 'If-Unmodified-Since' not in r2.json['headers']
def test_session_by_path(self, httpbin):
self.start_session(httpbin)
session_path = os.path.join(self.config_dir, 'session-by-path.json')
r1 = http('--session=' + session_path, 'GET', httpbin.url + '/get',
'Foo:Bar', env=self.env())
assert HTTP_OK in r1
r2 = http('--session=' + session_path, 'GET', httpbin.url + '/get',
env=self.env())
assert HTTP_OK in r2
assert r2.json['headers']['Foo'] == 'Bar'
@pytest.mark.skipif(
sys.version_info >= (3,),
reason="This test fails intermittently on Python 3 - "
"see https://github.com/jkbrzt/httpie/issues/282")
def test_session_unicode(self, httpbin):
self.start_session(httpbin)
r1 = http('--session=test', u'--auth=test:' + UNICODE,
'GET', httpbin.url + '/get', u'Test:%s' % UNICODE,
env=self.env())
assert HTTP_OK in r1
r2 = http('--session=test', '--verbose', 'GET',
httpbin.url + '/get', env=self.env())
assert HTTP_OK in r2
# FIXME: Authorization *sometimes* is not present on Python3
assert (r2.json['headers']['Authorization'] ==
HTTPBasicAuth.make_header(u'test', UNICODE))
# httpbin doesn't interpret utf8 headers
assert UNICODE in r2
def test_session_default_header_value_overwritten(self, httpbin):
self.start_session(httpbin)
# https://github.com/jkbrzt/httpie/issues/180
r1 = http('--session=test',
httpbin.url + '/headers', 'User-Agent:custom',
env=self.env())
assert HTTP_OK in r1
assert r1.json['headers']['User-Agent'] == 'custom'
r2 = http('--session=test', httpbin.url + '/headers', env=self.env())
assert HTTP_OK in r2
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)

93
tests/test_ssl.py Normal file
View File

@ -0,0 +1,93 @@
import os
import pytest
import pytest_httpbin.certs
from requests.exceptions import SSLError
from httpie import ExitStatus
from httpie.input import SSL_VERSION_ARG_MAPPING
from utils import http, HTTP_OK, TESTS_ROOT
CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
# FIXME:
# We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error.
# See: https://github.com/kevin1024/pytest-httpbin#https-support
CA_BUNDLE = pytest_httpbin.certs.where()
@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):
r = http(httpbin_secure + '/get',
'--cert', '/__not_found__',
error_exit_ok=True)
assert r.exit_status == ExitStatus.ERROR
assert 'No such file or directory' in r.stderr
def test_cert_file_invalid(self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure + '/get',
'--cert', __file__)
def test_cert_ok_but_missing_key(self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure + '/get',
'--cert', CLIENT_CERT)
class TestServerCert:
def test_verify_no_OK(self, httpbin_secure):
r = http(httpbin_secure.url + '/get', '--verify=no')
assert HTTP_OK in r
def test_verify_custom_ca_bundle_path(
self, httpbin_secure_untrusted):
r = http(httpbin_secure_untrusted + '/get', '--verify', CA_BUNDLE)
assert HTTP_OK in r
def test_self_signed_server_cert_by_default_raises_ssl_error(
self,
httpbin_secure_untrusted):
with pytest.raises(SSLError):
http(httpbin_secure_untrusted.url + '/get')
def test_verify_custom_ca_bundle_invalid_path(self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure.url + '/get', '--verify', '/__not_found__')
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
with pytest.raises(SSLError):
http(httpbin_secure.url + '/get', '--verify', __file__)

44
tests/test_stream.py Normal file
View File

@ -0,0 +1,44 @@
import pytest
from httpie.compat import is_windows
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import http, TestEnvironment
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
# GET because httpbin 500s with binary POST body.
@pytest.mark.skipif(is_windows,
reason='Pretty redirect not supported under Windows')
def test_pretty_redirected_stream(httpbin):
"""Test that --stream works with prettified redirected output."""
with open(BIN_FILE_PATH, 'rb') as f:
env = TestEnvironment(colors=256, stdin=f,
stdin_isatty=False,
stdout_isatty=False)
r = http('--verbose', '--pretty=all', '--stream', 'GET',
httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_encoded_stream(httpbin):
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
env = TestEnvironment(stdin=f, stdin_isatty=False)
r = http('--pretty=none', '--stream', '--verbose', 'GET',
httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r
def test_redirected_stream(httpbin):
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
env = TestEnvironment(stdout_isatty=False,
stdin_isatty=False,
stdin=f)
r = http('--pretty=none', '--stream', '--verbose', 'GET',
httpbin.url + '/get', env=env)
assert BIN_FILE_CONTENT in r

97
tests/test_unicode.py Normal file
View File

@ -0,0 +1,97 @@
# coding=utf-8
"""
Various unicode handling related tests.
"""
from utils import http, HTTP_OK
from fixtures import UNICODE
def test_unicode_headers(httpbin):
# httpbin doesn't interpret utf8 headers
r = http(httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r
def test_unicode_headers_verbose(httpbin):
# httpbin doesn't interpret utf8 headers
r = http('--verbose', httpbin.url + '/headers', u'Test:%s' % UNICODE)
assert HTTP_OK in r
assert UNICODE in r
def test_unicode_form_item(httpbin):
r = http('--form', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r
assert r.json['form'] == {'test': UNICODE}
def test_unicode_form_item_verbose(httpbin):
r = http('--verbose', '--form',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r
assert UNICODE in r
def test_unicode_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r
assert r.json['json'] == {'test': UNICODE}
def test_unicode_json_item_verbose(httpbin):
r = http('--verbose', '--json',
'POST', httpbin.url + '/post', u'test=%s' % UNICODE)
assert HTTP_OK in r
assert UNICODE in r
def test_unicode_raw_json_item(httpbin):
r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_raw_json_item_verbose(httpbin):
r = http('--json', 'POST', httpbin.url + '/post',
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
assert HTTP_OK in r
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
def test_unicode_url_query_arg_item(httpbin):
r = http(httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE}, r
def test_unicode_url_query_arg_item_verbose(httpbin):
r = http('--verbose', httpbin.url + '/get', u'test==%s' % UNICODE)
assert HTTP_OK in r
assert UNICODE in r
def test_unicode_url(httpbin):
r = http(httpbin.url + u'/get?test=' + UNICODE)
assert HTTP_OK in r
assert r.json['args'] == {'test': UNICODE}
# def test_unicode_url_verbose(self):
# r = http(httpbin.url + '--verbose', u'/get?test=' + UNICODE)
# assert HTTP_OK in r
def test_unicode_basic_auth(httpbin):
# it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth
http('--verbose', '--auth', u'test:%s' % UNICODE,
httpbin.url + u'/basic-auth/test/' + UNICODE)
def test_unicode_digest_auth(httpbin):
# it doesn't really authenticate us because httpbin
# doesn't interpret the utf8-encoded auth
http('--auth-type=digest',
'--auth', u'test:%s' % UNICODE,
httpbin.url + u'/digest-auth/auth/test/' + UNICODE)

75
tests/test_uploads.py Normal file
View File

@ -0,0 +1,75 @@
import os
import pytest
from httpie.input import ParseError
from utils import TestEnvironment, http, HTTP_OK
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
class TestMultipartFormDataFileUpload:
def test_non_existent_file_raises_parse_error(self, httpbin):
with pytest.raises(ParseError):
http('--form',
'POST', httpbin.url + '/post', 'foo@/__does_not_exist__')
def test_upload_ok(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
'test-file@%s' % FILE_PATH_ARG, 'foo=bar')
assert HTTP_OK in r
assert 'Content-Disposition: form-data; name="foo"' in r
assert 'Content-Disposition: form-data; name="test-file";' \
' filename="%s"' % os.path.basename(FILE_PATH) in r
assert FILE_CONTENT 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):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
'test-file@%s' % FILE_PATH_ARG,
'test-file@%s' % FILE_PATH_ARG)
assert HTTP_OK in r
assert r.count('Content-Disposition: form-data; name="test-file";'
' filename="%s"' % os.path.basename(FILE_PATH)) == 2
# Should be 4, but is 3 because httpbin
# doesn't seem to support filed field lists
assert r.count(FILE_CONTENT) in [3, 4]
assert r.count('Content-Type: text/plain') == 2
class TestRequestBodyFromFilePath:
"""
`http URL @file'
"""
def test_request_body_from_file_by_path(self, httpbin):
r = http('--verbose',
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG)
assert HTTP_OK in r
assert FILE_CONTENT in r, r
assert '"Content-Type": "text/plain"' in r
def test_request_body_from_file_by_path_with_explicit_content_type(
self, httpbin):
r = http('--verbose',
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG,
'Content-Type:text/plain; charset=utf8')
assert HTTP_OK in r
assert FILE_CONTENT in r
assert 'Content-Type: text/plain; charset=utf8' in r
def test_request_body_from_file_by_path_no_field_name_allowed(
self, httpbin):
env = TestEnvironment(stdin_isatty=True)
r = http('POST', httpbin.url + '/post', 'field-name@' + FILE_PATH_ARG,
env=env, error_exit_ok=True)
assert 'perhaps you meant --form?' in r.stderr
def test_request_body_from_file_by_path_no_data_items_allowed(
self, httpbin):
env = TestEnvironment(stdin_isatty=False)
r = http('POST', httpbin.url + '/post', '@' + FILE_PATH_ARG, 'foo=bar',
env=env, error_exit_ok=True)
assert 'cannot be mixed' in r.stderr

31
tests/test_windows.py Normal file
View File

@ -0,0 +1,31 @@
import os
import tempfile
import pytest
from httpie.context import Environment
from utils import TestEnvironment, http
from httpie.compat import is_windows
@pytest.mark.skipif(not is_windows, reason='windows-only')
class TestWindowsOnly:
@pytest.mark.skipif(True,
reason='this test for some reason kills the process')
def test_windows_colorized_output(self, httpbin):
# Spits out the colorized output.
http(httpbin.url + '/get', env=Environment())
class TestFakeWindows:
def test_output_file_pretty_not_allowed_on_windows(self, httpbin):
env = TestEnvironment(is_windows=True)
output_file = os.path.join(
tempfile.gettempdir(),
self.test_output_file_pretty_not_allowed_on_windows.__name__
)
r = http('--output', output_file,
'--pretty=all', 'GET', httpbin.url + '/get',
env=env, error_exit_ok=True)
assert 'Only terminal output can be colorized on Windows' in r.stderr

View File

@ -1,193 +0,0 @@
# coding:utf-8
import os
import sys
import unittest
import argparse
from requests.compat import is_py26
import tempfile
TESTS_ROOT = os.path.dirname(__file__)
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import __main__
from httpie import cliparse
TEST_FILE = os.path.join(TESTS_ROOT, 'file.txt')
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
def http(*args, **kwargs):
http_kwargs = {
'stdin_isatty': True,
'stdout_isatty': False
}
http_kwargs.update(kwargs)
stdout = http_kwargs.setdefault('stdout', tempfile.TemporaryFile())
__main__.main(args=args, **http_kwargs)
stdout.seek(0)
response = stdout.read().decode('utf8')
stdout.close()
return response
class BaseTest(unittest.TestCase):
if is_py26:
def assertIn(self, member, container, msg=None):
self.assert_(member in container, msg)
def assertNotIn(self, member, container, msg=None):
self.assert_(member not in container, msg)
def assertDictEqual(self, d1, d2, msg=None):
self.assertEqual(set(d1.keys()), set(d2.keys()), msg)
self.assertEqual(sorted(d1.values()), sorted(d2.values()), msg)
class TestItemParsing(BaseTest):
def setUp(self):
self.key_value_type = cliparse.KeyValueType(
cliparse.SEP_HEADERS,
cliparse.SEP_DATA,
cliparse.SEP_DATA_RAW_JSON,
cliparse.SEP_FILES,
)
def test_invalid_items(self):
items = ['no-separator']
for item in items:
self.assertRaises(argparse.ArgumentTypeError,
lambda: self.key_value_type(item))
def test_escape(self):
headers, data, files = cliparse.parse_items([
# headers
self.key_value_type('foo\\:bar:baz'),
self.key_value_type('jack\\@jill:hill'),
# data
self.key_value_type('baz\\=bar=foo'),
# files
self.key_value_type('bar\\@baz@%s' % TEST_FILE)
])
self.assertDictEqual(headers, {
'foo:bar': 'baz',
'jack@jill': 'hill',
})
self.assertDictEqual(data, {
'baz=bar': 'foo',
})
self.assertIn('bar@baz', files)
def test_escape_longsep(self):
headers, data, files = cliparse.parse_items([
self.key_value_type('bob\\:==foo'),
])
self.assertDictEqual(data, {
'bob:=': 'foo',
})
def test_valid_items(self):
headers, data, files = cliparse.parse_items([
self.key_value_type('string=value'),
self.key_value_type('header:value'),
self.key_value_type('list:=["a", 1, {}, false]'),
self.key_value_type('obj:={"a": "b"}'),
self.key_value_type('eh:'),
self.key_value_type('ed='),
self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE),
])
self.assertDictEqual(headers, {
'header': 'value',
'eh': ''
})
self.assertDictEqual(data, {
"ed": "",
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"}
})
self.assertIn('test-file', files)
class TestHTTPie(BaseTest):
def test_get(self):
http('GET', 'http://httpbin.org/get')
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertEqual(r.count('__test__'), 2)
def test_json(self):
response = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
response2 = http('-j', 'GET', 'http://httpbin.org/headers')
self.assertIn('"Accept": "application/json"', response2)
response3 = http('-j', 'GET', 'http://httpbin.org/headers', 'Accept:application/xml')
self.assertIn('"Accept": "application/xml"', response3)
def test_form(self):
response = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
def test_headers(self):
response = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"User-Agent": "HTTPie', response)
self.assertIn('"Foo": "bar"', response)
class TestPrettyFlag(BaseTest):
"""Test the --pretty / --ugly flag handling."""
def test_pretty_enabled_by_default(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_pretty(self):
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_ugly(self):
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class TestFileUpload(BaseTest):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(cliparse.ParseError, http,
'--form', '--traceback',
'POST', 'http://httpbin.org/post',
'foo@/__does_not_exist__')
def test_upload_ok(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
'test-file@%s' % TEST_FILE)
self.assertIn('"test-file": "__test_file_content__', r)
class TestAuth(BaseTest):
def test_basic_auth(self):
r = http('--auth', 'user:password',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_digest_auth(self):
r = http('--auth-type=digest', '--auth', 'user:password',
'GET', 'httpbin.org/digest-auth/auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
if __name__ == '__main__':
unittest.main()

254
tests/utils.py Normal file
View File

@ -0,0 +1,254 @@
# coding=utf-8
"""Utilities for HTTPie test suite."""
import os
import sys
import time
import json
import tempfile
from httpie import ExitStatus, EXIT_STATUS_LABELS
from httpie.context import Environment
from httpie.core import main
from httpie.compat import bytes, str
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
CRLF = '\r\n'
COLOR = '\x1b['
HTTP_OK = '200 OK'
HTTP_OK_COLOR = (
'HTTP\x1b[39m\x1b[38;5;245m/\x1b[39m\x1b'
'[38;5;37m1.1\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;37m200'
'\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;136mOK'
)
def mk_config_dir():
dirname = tempfile.mkdtemp(prefix='httpie_config_')
return dirname
def add_auth(url, auth):
proto, rest = url.split('://', 1)
return proto + '://' + auth + '@' + rest
class TestEnvironment(Environment):
"""Environment subclass with reasonable defaults for testing."""
colors = 0
stdin_isatty = True,
stdout_isatty = True
is_windows = False
def __init__(self, **kwargs):
if 'stdout' not in kwargs:
kwargs['stdout'] = tempfile.TemporaryFile(
mode='w+b',
prefix='httpie_stdout'
)
if 'stderr' not in kwargs:
kwargs['stderr'] = tempfile.TemporaryFile(
mode='w+t',
prefix='httpie_stderr'
)
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):
try:
self.cleanup()
except Exception:
pass
class BaseCLIResponse(object):
"""
Represents the result of simulated `$ http' invocation via `http()`.
Holds and provides access to:
- stdout output: print(self)
- stderr output: print(self.stderr)
- exit_status output: print(self.exit_status)
"""
stderr = None
json = None
exit_status = None
class BytesCLIResponse(bytes, BaseCLIResponse):
"""
Used as a fallback when a StrCLIResponse cannot be used.
E.g. when the output contains binary data or when it is colorized.
`.json` will always be None.
"""
class StrCLIResponse(str, BaseCLIResponse):
@property
def json(self):
"""
Return deserialized JSON body, if one included in the output
and is parseable.
"""
if not hasattr(self, '_json'):
self._json = None
# De-serialize JSON body if possible.
if COLOR in self:
# Colorized output cannot be parsed.
pass
elif self.strip().startswith('{'):
# Looks like JSON body.
self._json = json.loads(self)
elif (self.count('Content-Type:') == 1 and
'application/json' in self):
# Looks like a whole JSON HTTP message,
# try to extract its body.
try:
j = self.strip()[self.strip().rindex('\r\n\r\n'):]
except ValueError:
pass
else:
try:
self._json = json.loads(j)
except ValueError:
pass
return self._json
class ExitStatusError(Exception):
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 not error_exit_ok and '--traceback' not in args_with_config_defaults:
add_to_args.append('--traceback')
if not any('--timeout' in arg for arg in args_with_config_defaults):
add_to_args.append('--timeout=3')
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()

30
tox.ini Normal file
View File

@ -0,0 +1,30 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. See ./CONTRIBUTING.rst
[tox]
envlist = py26, py27, py35, pypy, codestyle
[testenv]
deps =
mock
pytest
pytest-httpbin>=0.0.6
commands =
# 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