Compare commits

...

136 Commits
0.1.5 ... 0.2.6

Author SHA1 Message Date
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
a0700c41ad 0.2.1 2012-06-13 16:01:23 +02:00
e175fe9d0e Ensured a new line after the request message in the output. 2012-06-13 15:32:02 +02:00
d544ec3823 Made --verbose work also with requests<0.12.1. 2012-06-13 15:25:05 +02:00
6cf2910de0 Version bump to 0.2.1dev. 2012-06-13 15:24:48 +02:00
f64eb09571 Merge pull request #50 from dair-targ/master
Fixed --verbose flag for newer requests.
2012-06-13 06:14:12 -07:00
126130455e Merge pull request #45 from gandaro/pygments-lexers
Use the Pygments HTTP and JSON lexers
2012-06-13 06:09:11 -07:00
70b3658004 --verbose flag was not working. Here is bugfix. 2012-06-02 23:14:21 +04:00
d89eeb0796 PEP-8 2012-04-28 14:18:59 +02:00
bced559496 use PrettyHttp class; working --headers and --body 2012-04-28 14:16:47 +02:00
4aa86cb438 Use the full capability of HttpLexer 2012-04-26 14:48:38 +02:00
2d7f2c65a2 Use the Pygments HTTP and JSON lexers 2012-04-26 13:05:59 +02:00
3d11042772 Fixed classifiers in setup.py. 2012-04-25 02:16:10 +02:00
b8cc7c205c Updated README. 2012-04-25 02:13:39 +02:00
3a96706e18 0.2.0 2012-04-25 02:11:19 +02:00
7910269996 Updated README and docs. 2012-04-25 02:10:58 +02:00
c5b1aaaa28 Major clean-up and refactoring. 2012-04-25 01:32:53 +02:00
67d6426360 Fixed several unicode-related issues
Closes #31 Python 3 & non-ascii arguments => UnicodeEncodeError
Closes #41 Unicode response error.
Closes #42 UnicodeEncodeError when piping Unicode output
2012-04-25 00:08:40 +02:00
29e594daaf Merge pull request #44 from jakebasile/master
Escaping separators
2012-04-24 07:25:17 -07:00
90af1f7422 Fixed escaping for long separators. 2012-04-18 18:18:00 -05:00
16df8848e8 Removed accidentally included old funky code. 2012-04-16 20:47:13 -05:00
c29981c633 Added ability to escape parameters... except for the := ones. 2012-04-16 20:28:08 -05:00
6db93b25d8 Merge pull request #43 from jakebasile/master
-j/--json sets Accept header
2012-04-14 14:21:08 -07:00
45ce446017 -j/--json now adds "Accept": "application/json" to GET requests if no previous Accept header exists. 2012-04-14 14:13:53 -05:00
4da3821bc4 Lowered the minimum version of requests required
So that the Debian package works out of the box:
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=663506
2012-04-11 22:21:11 +02:00
0c4c6c4753 Added --auth-type and tests for basic/digest auth.
Closes #38.
2012-04-11 13:56:25 +02:00
ee598d304d Merge branch 'digest-auth' of https://github.com/dshafik/httpie into dshafik-digest-auth 2012-04-11 13:16:26 +02:00
c6c1489212 Refactored @mmb's fix to --verify; updated docs.
Closes #32.
2012-04-11 12:46:08 +02:00
71d21d1feb make --verify no command line argument work 2012-04-05 12:55:05 -04:00
153663cb92 Add support for Digest authentication 2012-03-22 10:51:33 -04:00
1c5fd18465 Python 3 fixes (travis config). 2012-03-15 00:31:12 +01:00
ab23037582 Python 3 fixes. 2012-03-15 00:28:15 +01:00
3dbb61a8ca Added argparse for Python 3.1. 2012-03-15 00:22:29 +01:00
51aa0409e6 Added Python 3 support
Closes #11.
2012-03-15 00:13:11 +01:00
1f49900db6 Improved README. 2012-03-14 22:55:09 +01:00
e2e749b2a6 Improved request formatting. 2012-03-14 22:45:35 +01:00
ccbea8c96e Assume "/" as the Request-URI for printing when none present. 2012-03-14 19:30:12 +01:00
6a1f0248e1 Fixed tests. 2012-03-14 19:21:47 +01:00
b7e0473d6c Added file upload support
It is now possible to send multipart/form-data requests.

Note that the --file option used previously has been removed
because it didn't allow you specify the field name.

Example:

    http -f POST example.com field-name@/path/to/file
2012-03-14 19:14:37 +01:00
578acacdf3 Added a --verbose / -v flag
When set, the whole request as well as the response is printed. Shortcut for --print=HBhb.
2012-03-14 11:15:21 +01:00
ed888a2657 Made sure request Host is correct when printing. 2012-03-14 01:12:10 +01:00
5e19e1b95d Added a "New in development version" link. 2012-03-14 00:58:05 +01:00
02622a4135 Added the option to print the request
It is now possible to print any combination of the following
request-response bits:

    - Request headers (H)
    - Request body (B)
    - Response headers (h)
    - Response body (b)

The output is controlled by the --print / -p option which
defaults to "hb" (i.e., response headers and response body).

Note that -p was previously shortcut for --prety.

Closes #29.
2012-03-14 00:44:13 +01:00
31c28807c9 Added better JSON highlighting
A JSON-specific lexer for Pygments by Norman Richards (@orb)
has been added. It attempts to provide more interesting syntax
highlighting which correctly distinguishes between attribute
names and values.

Closes #25.
2012-03-13 21:45:40 +01:00
78e20c6e85 Merge remote-tracking branch 'upstream/master' 2012-03-13 11:00:56 -04:00
20408e12e9 Added BSD license text
HTTPie is going to be packaged for Debian and this will make it easier. The license and © is still the same as before.
2012-03-13 10:59:29 -04:00
3c8af4c170 Added BSD license text
HTTPie is going to be packaged for Debian and this will make it easier. The license and © is still the same as before.
2012-03-09 14:20:26 +01:00
c9eb2255f6 Added proper JSON highlighting 2012-03-05 12:58:21 -05:00
028d16d0ff Merge pull request #24 from faulkner/fix-packaging
Update references to moved README.
Closes #23, #24.
2012-03-05 00:04:53 -08:00
f5d4addab2 Update references to moved README. 2012-03-04 22:26:23 -08:00
ccb2aaf94f Update README.rst 2012-03-05 02:54:34 +01:00
8cff0a3e67 Updated README. 2012-03-05 00:48:06 +01:00
ce952c9e90 Added support for more 256 color terminals.
Closes #20. Thanks, @laurentb.
2012-03-04 16:40:02 +01:00
e83e601f7c Added a link to contributors on GitHub to README. 2012-03-04 16:39:57 +01:00
f995c61892 Merge pull request #21 from laurentb/packaging
Packaging fixes
2012-03-04 07:35:24 -08:00
71771dc4a6 One argument per line 2012-03-04 16:28:30 +01:00
1517f3d149 Include README.md in the source tarball
This should fix the IOError that occured in setup.py.
2012-03-04 16:28:30 +01:00
d9abf7d31c Updated screenshot. 2012-03-04 15:37:04 +01:00
c446d756ab Fixed IOError in setup.py. 2012-03-04 13:47:09 +01:00
22 changed files with 2420 additions and 523 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ dist
httpie.egg-info
build
*.pyc
.tox

View File

@ -2,10 +2,8 @@ language: python
python:
- 2.6
- 2.7
# TODO: Python 3
#- 3.2
script: python tests.py
- pypy
- 3.2
script: python setup.py test
install:
- pip install requests pygments
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse; fi"
- pip install . --use-mirrors

26
LICENSE Normal file
View File

@ -0,0 +1,26 @@
Copyright © 2012 Jakub Roztocil <jakub@roztocil.name>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of The author nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include README.rst LICENSE

107
README.md
View File

@ -1,107 +0,0 @@
## HTTPie: cURL for humans
[![Build Status](https://secure.travis-ci.org/jkbr/httpie.png)](http://travis-ci.org/jkbr/httpie)
HTTPie is a CLI frontend for [python-requests](http://python-requests.org) built out of frustration. It provides an `http` command that can be used to easily issue HTTP requests. It is meant to be used by humans to interact with HTTP-based APIs and web servers. The response headers are colorized and the body is syntax-highlighted if its `Content-Type` is known to [Pygments](http://pygments.org/) (unless the output is redirected).
![httpie](https://github.com/jkbr/httpie/raw/master/httpie.png)
### Installation
Latest stable version using [pip](http://www.pip-installer.org/en/latest/index.html):
pip install -U httpie
# easy_install httpie
Master:
pip install -U https://github.com/jkbr/httpie/tarball/master
### Usage
http [flags] METHOD URL [header:value | data-field-name=value]*
The default request `Content-Type` is `application/json` and data fields are automatically serialized as a JSON `Object`, so this:
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org
Will issue the following request:
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/json; charset=utf-8
{"name": "John", "email": "john@example.org"}
You can pass other types than just strings using the `field:=value` notation. It allows you to set arbitrary JSON to the data fields:
http PUT httpie.org/pies bool:=true list:=[1,2,3] 'object:={"a": "b", "c": "d"}'
Produces the following JSON request:
{"bool": true, "list": [1, 2, 3], "object": {"a": "b", "c": "d"}}
You can use the `--form` flag to set `Content-Type` and serialize the data as `application/x-www-form-urlencoded`.
The data to be sent can also be passed via `stdin`:
http PUT api.example.com/person/1 X-API-Token:123 < person.json
Most of the flags mirror the arguments you would use with `requests.request`. See `http -h`:
usage: http [-h] [--version] [--json | --form] [--traceback]
[--pretty | --ugly] [--headers | --body] [--style STYLE]
[--auth AUTH] [--verify VERIFY] [--proxy PROXY]
[--allow-redirects] [--file PATH] [--timeout TIMEOUT]
METHOD URL [items [items ...]]
HTTPie - cURL for humans.
positional arguments:
METHOD HTTP method to be used for the request (GET, POST,
PUT, DELETE, PATCH, ...).
URL Protocol defaults to http:// if the URL does not
include it.
items HTTP header (key:value), data field (key=value) or raw
JSON field (field:=value).
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--json, -j Serialize data items as a JSON object and set Content-
Type to application/json, if not specified.
--form, -f Serialize data items as form values and set Content-
Type to application/x-www-form-urlencoded, if not
specified.
--traceback Print exception traceback should one occur.
--pretty, -p 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.
--ugly, -u Do not prettify the response.
--headers, -t Print only the response headers.
--body, -b Print only the response body.
--style STYLE, -s STYLE
Output coloring style, one of autumn, borland, bw,
colorful, default, emacs, friendly, fruity, manni,
monokai, murphy, native, pastie, perldoc, solarized,
tango, trac, vim, vs. Defaults to solarized.
--auth AUTH, -a AUTH username:password
--verify VERIFY Set to "yes" to check 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.
--proxy PROXY String mapping protocol to the URL of the proxy (e.g.
http:foo.bar:3128).
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
POST-ing of data at new ``Location``)
--file PATH File to multipart upload
--timeout TIMEOUT Float describes the timeout of the request (Use
socket.setdefaulttimeout() as fallback).
### Changelog
* [0.1.5](https://github.com/jkbr/httpie/compare/0.1.4...0.1.5) (2012-03-04)

401
README.rst Normal file
View File

@ -0,0 +1,401 @@
=======================
HTTPie: cURL for humans
=======================
**HTTPie is a CLI HTTP utility** built out of frustration with existing tools.
Its goal is to make CLI interaction with HTTP-based services as
**human-friendly** as possible. HTTPie provides an ``http`` command that allows
for issuing **arbitrary HTTP** requests using a **simple and natural syntax**,
and displays **colorized responses**:
.. image:: https://github.com/jkbr/httpie/raw/master/httpie.png
:alt: HTTPie compared to cURL
HTTPie supports Python 2.6+ (including Python 3.x and PyPy) and has been tested
under Mac OS X, Linux and Windows. It also has a
comprehensive `suite of tests`_ with `continuous integration`_.
Under the hood, the excellent `Requests`_ and `Pygments`_ Python libraries
are used.
Installation
============
The latest **stable version** of HTTPie can always be installed or updated
to via `pip`_ (prefered)
or ``easy_install``::
pip install -U httpie
# easy_install pip
Or, you can install the **development version** directly from GitHub:
.. image:: https://secure.travis-ci.org/jkbr/httpie.png
:target: http://travis-ci.org/jkbr/httpie
:alt: Build Status of the master branch
::
pip install -U https://github.com/jkbr/httpie/tarball/master
There are also packages available for `Ubuntu`_, `Debian`_ and possibly other
distributions as well.
Usage
=====
Hello world::
http httpie.org
Synopsis::
http [flags] [METHOD] URL [items]
There are five different types of key/value pair ``items`` available:
+-----------------------+-----------------------------------------------------+
| **Headers** | Arbitrary HTTP headers. The ``:`` character is |
| ``Name:Value`` | used to separate a header's name from its value, |
| | e.g., ``X-API-Token:123``. |
+-----------------------+-----------------------------------------------------+
| **Simple data | Included in the request body and depending on the |
| fields** | ``Content-Type`` they are automatically serialized |
| ``field=value`` | as a JSON ``Object`` (default) or |
| | ``application/x-www-form-urlencoded`` |
| | (``--form``/ ``-f``). Data items use ``=`` |
| | as the separator, e.g., ``hello=world``. |
+-----------------------+-----------------------------------------------------+
| **Raw JSON fields** | Useful when the ``Content-Type`` is JSON and one or |
| ``field:=json`` | more fields need to be a ``Boolean``, ``Number``, |
| | nested ``Object``, or an ``Array``. It's because |
| | simple data items are always serialized as a |
| | ``String``. E.g., ``pies:=[1,2,3]``, or |
| | ``'meals:=["ham","spam"]'`` (note the quotes). |
| | It may be more convenient to pass the whole JSON |
| | body via ``stdin`` when it's more complex |
| | (see examples bellow). |
+-----------------------+-----------------------------------------------------+
| **File fields** | Only available with ``-f`` / ``--form``. Use ``@`` |
| ``field@/dir/file`` | as the separator, e.g., |
| | ``screenshot@~/Pictures/img.png``. |
| | The presence of a file field results |
| | into a ``multipart/form-data`` request. |
+-----------------------+-----------------------------------------------------+
| **Query string | Appends the given name/value pair as a query |
| parameters** | string parameter to the URL. |
| ``name==value`` | The ``==`` separator is used |
+-----------------------+-----------------------------------------------------+
All ``items`` come after the URL, and, unlike ``flags``, they become part of
the actual request being is sent. Their types are distinguished by the
separator used.
Examples
--------
::
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org age:=29
The following request is issued::
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/json; charset=utf-8
{"name": "John", "email": "john@example.org", "age": 29}
It can easily be changed to a **form** request using the ``-f``
(or ``--form``) flag, which produces::
PATCH /person/1 HTTP/1.1
User-Agent: HTTPie/0.1
X-API-Token: 123
Content-Type: application/x-www-form-urlencoded; charset=utf-8
age=29&name=John&email=john%40example.org
It is also possible to send ``multipart/form-data`` requests, i.e., to
simulate a **file upload form** submission. It is done using the
``--form`` / ``-f`` flag and passing one or more file fields::
http -f POST example.com/jobs name=John cv@~/Documents/cv.pdf
The above will send the same request as if the following HTML form were
submitted::
<form enctype="multipart/form-data" method="post" action="http://example.com/jobs">
<input type="text" name="name" />
<input type="file" name="cv" />
</form>
**Query string parameters** can be added to any request without having to
escape the ``&`` characters. The following request will contain
``?search=donuts&in=fridge`` as the query string part of the URL::
http GET example.com search==donuts in==fridge
The whole request body can also be passed in **via stdin,** in which
case it will be used with no further processing::
echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
# Or:
http POST example.com/person/1 X-API-Token:123 < person.json
That can be used for **piping services together**. The following example
``GET``-s JSON data from the Github API and ``POST``-s it to httpbin.org::
http GET https://api.github.com/repos/jkbr/httpie | http POST httpbin.org/post
The above can be further simplified by omitting ``GET`` and ``POST`` because
they are both default here as the first command has no request data whereas
the second one has via ``stdin``::
http https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
Note that when the **output is redirected** (like the examples above), HTTPie
applies a different set of defaults than for a console output. Namely, colors
aren't used (unless ``--pretty`` is set) and only the response body
is printed (unless ``--print`` options specified).
An alternative to ``stdin`` is to pass a filename whose content will be used
as the request body. It has the advantage that the ``Content-Type`` header
will automatically be set to the appropriate value based on the filename
extension. Thus, the following will request will send the verbatim contents
of the file with ``Content-Type: application/xml``::
http PUT httpbin.org/put @/data/file.xml
When using HTTPie from **shell scripts** it can be useful to use the
``--check-status`` flag. It instructs HTTPie to exit with an error if the
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``,
respectively::
#!/bin/bash
if http --check-status HEAD example.org/health &> /dev/null; then
echo 'OK!'
else
case $? in
3) echo 'Unexpected 3xx Redirection!' ;;
4) echo '4xx Client Error!' ;;
5) echo '5xx Server Error!' ;;
*) echo 'Other Error!' ;;
esac
fi
Flags
-----
``$ http --help``::
usage: http [--help] [--version] [--json | --form] [--traceback]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--check-status] [--auth AUTH]
[--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY]
[--allow-redirects] [--timeout TIMEOUT]
[METHOD] URL [ITEM [ITEM ...]]
HTTPie - cURL for humans. <http://httpie.org>
positional arguments:
METHOD The HTTP method to be used for the request (GET, POST,
PUT, DELETE, PATCH, ...). If this argument is omitted,
then HTTPie will guess the HTTP method. If there is
some data to be sent, then it will be POST, otherwise
GET.
URL The protocol defaults to http:// if the URL does not
include one.
ITEM 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), a query parameter (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.
optional arguments:
--help show this help message and exit
--version show program's version number and exit
--json, -j (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).
--form, -f 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 into a multipart/form-data
request.
--traceback Print exception traceback should one occur.
--pretty 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.
--ugly, -u Do not prettify the response.
--print OUTPUT_OPTIONS, -p OUTPUT_OPTIONS
String specifying what the output should contain: "H"
stands for the request headers, and "B" for the
request body. "h" stands for the response headers and
"b" for response the body. The default behaviour is
"hb" (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
body is printed by default.
--verbose, -v Print the whole request as well as the response.
Shortcut for --print=HBhb.
--headers, -h Print only the response headers. Shortcut for
--print=h.
--body, -b Print only the response body. Shortcut for --print=b.
--style STYLE, -s STYLE
Output coloring style, one of autumn, borland, bw,
colorful, default, emacs, friendly, fruity, manni,
monokai, murphy, native, pastie, perldoc, rrt,
solarized, tango, trac, vim, vs. 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).
--check-status 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
--allow-redirects hasn't been set, then the exit
status is 3. Also an error message is written to
stderr if stdout is redirected.
--auth AUTH, -a AUTH username:password. If only the username is provided
(-a username), HTTPie will prompt for the password.
--auth-type {basic,digest}
The authentication mechanism to be used. Defaults to
"basic".
--verify VERIFY 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".
--proxy PROXY String mapping protocol to the URL of the proxy (e.g.
http:foo.bar:3128).
--allow-redirects Set this flag if full redirects are allowed (e.g. re-
POST-ing of data at new ``Location``)
--timeout TIMEOUT Float describes the timeout of the request (Use
socket.setdefaulttimeout() as fallback).
Contribute
==========
Bug reports and code and documentation patches are greatly appretiated. You can
also help by using the development version of HTTPie and reporting any bugs you
might encounter.
Before working on a new feature or a bug, please browse the `existing issues`_
to see whether it has been previously discussed.
Then fork and clone `the repository`_.
To point the ``http`` command to your local branch during development you can
install HTTPie in an editable mode::
pip install --editable .
To run the existing suite of tests before a pull request is submitted::
python setup.py test
`Tox`_ can also be used to conveniently run tests in all of the
`supported Python environments`_::
# Install tox
pip install tox
# Run tests
tox
Changelog
=========
* `0.2.7dev`_
* `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)
Authors
=======
`Jakub Roztocil`_ (`@jkbrzt`_) created HTTPie and
`these fine people <https://github.com/jkbr/httpie/contributors>`_
have contributed.
.. _suite of tests: https://github.com/jkbr/httpie/blob/master/tests/tests.py
.. _continuous integration: http://travis-ci.org/#!/jkbr/httpie
.. _Requests: http://python-requests.org
.. _Pygments: http://pygments.org/
.. _pip: http://www.pip-installer.org/en/latest/index.html
.. _Tox: http://tox.testrun.org
.. _supported Python environments: https://github.com/jkbr/httpie/blob/master/tox.ini
.. _Ubuntu: http://packages.ubuntu.com/httpie
.. _Debian: http://packages.debian.org/httpie
.. _the repository: https://github.com/jkbr/httpie
.. _Jakub Roztocil: http://roztocil.name
.. _@jkbrzt: https://twitter.com/jkbrzt
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
.. _0.2.0: https://github.com/jkbr/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jkbr/httpie/compare/0.2.0...0.2.1
.. _0.2.2: https://github.com/jkbr/httpie/compare/0.2.1...0.2.2
.. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5
.. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
.. _0.2.7dev: https://github.com/jkbr/httpie/compare/0.2.6...master

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -3,5 +3,5 @@ HTTPie - cURL for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.1.5'
__version__ = '0.2.6'
__licence__ = 'BSD'

View File

@ -1,116 +1,10 @@
#!/usr/bin/env python
import os
"""The main entry point. Invoke as `http' or `python -m httpie'.
"""
import sys
import json
import requests
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
from . import cli
from . import pretty
from . import __version__ as version
DEFAULT_UA = 'HTTPie/%s' % version
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; charset=utf-8'
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:])
do_prettify = (args.prettify is True or
(args.prettify == cli.PRETTIFY_STDOUT_TTY_ONLY and stdout_isatty))
# Parse request headers and data from the command line.
headers = CaseInsensitiveDict()
headers['User-Agent'] = DEFAULT_UA
data = OrderedDict()
try:
cli.parse_items(items=args.items, headers=headers, data=data)
except cli.ParseError as e:
if args.traceback:
raise
parser.error(e.message)
if not stdin_isatty:
if data:
parser.error('Request body (stdin) and request '
'data (key=value) cannot be mixed.')
data = stdin.read()
# JSON/Form content type.
if args.json or (not args.form and data):
if stdin_isatty:
data = json.dumps(data)
if 'Content-Type' not in headers and (data or args.json):
headers['Content-Type'] = TYPE_JSON
elif 'Content-Type' not in headers:
headers['Content-Type'] = TYPE_FORM
# Fire the request.
try:
response = requests.request(
method=args.method.lower(),
url=args.url if '://' in args.url else 'http://%s' % args.url,
headers=headers,
data=data,
verify=True if args.verify == 'yes' else args.verify,
timeout=args.timeout,
auth=(args.auth.key, args.auth.value) if args.auth else None,
proxies=dict((p.key, p.value) for p in args.proxy),
files=dict((os.path.basename(f.name), f) for f in args.file),
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)
# Reconstruct the raw response.
encoding = response.encoding or 'ISO-8859-1'
original = response.raw._original_response
status_line, headers, body = (
'HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status, reason=original.reason,
),
str(original.msg).decode(encoding),
response.content.decode(encoding) if response.content else u''
)
if do_prettify:
prettify = pretty.PrettyHttp(args.style)
if args.print_headers:
status_line = prettify.headers(status_line)
headers = prettify.headers(headers)
if args.print_body and 'Content-Type' in response.headers:
body = prettify.body(body, response.headers['Content-Type'])
# Output.
# TODO: preserve leading/trailing whitespaces in the body.
# Some of the Pygments styles add superfluous line breaks.
if args.print_headers:
stdout.write(status_line.strip())
stdout.write('\n')
stdout.write(headers.strip().encode('utf-8'))
stdout.write('\n\n')
if args.print_body:
stdout.write(body.strip().encode('utf-8'))
stdout.write('\n')
from .core import main
if __name__ == '__main__':
main()
sys.exit(main())

View File

@ -1,79 +1,26 @@
import json
import argparse
from collections import namedtuple
from . import pretty
from . import __doc__ as doc
from . import __version__ as version
"""CLI arguments definition.
NOTE: the CLI interface may change before reaching v1.0.
SEP_COMMON = ':'
SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
PRETTIFY_STDOUT_TTY_ONLY = object()
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
def __call__(self, string):
found = dict((string.find(sep), sep)
for sep in self.separators
if string.find(sep) != -1)
if not found:
#noinspection PyExceptionInherit
raise argparse.ArgumentTypeError(
'"%s" is not a valid value' % string)
sep = found[min(found.keys())]
key, value = string.split(sep, 1)
return KeyValue(key=key, value=value, sep=sep, orig=string)
def parse_items(items, data=None, headers=None):
"""Parse `KeyValueType` `items` into `data` and `headers`."""
if headers is None:
headers = {}
if data is None:
data = {}
for item in items:
value = item.value
if item.sep == SEP_HEADERS:
target = headers
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 item.key in target:
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
target[item.key] = value
return headers, data
"""
from . import __doc__
from . import __version__
from .output import AVAILABLE_STYLES
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
PRETTIFY_STDOUT_TTY_ONLY,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS)
def _(text):
"""Normalize white space."""
"""Normalize whitespace."""
return ' '.join(text.strip().split())
parser = argparse.ArgumentParser(description=doc.strip(),)
parser.add_argument('--version', action='version', version=version)
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
parser.add_argument('--version', action='version', version=__version__)
# Content type.
#############################################
@ -82,16 +29,20 @@ group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument(
'--json', '-j', action='store_true',
help=_('''
Serialize data items as a JSON object and set
Content-Type to application/json, if not specified.
(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).
''')
)
group_type.add_argument(
'--form', '-f', action='store_true',
help=_('''
Serialize data items as form values and set
Content-Type to application/x-www-form-urlencoded,
if not specified.
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
into a multipart/form-data request.
''')
)
@ -108,13 +59,12 @@ parser.add_argument(
prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument(
'--pretty', '-p', dest='prettify', action='store_true',
'--pretty', dest='prettify', action='store_true',
default=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.
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(
@ -124,46 +74,112 @@ prettify.add_argument(
''')
)
only = parser.add_mutually_exclusive_group(required=False)
only.add_argument(
'--headers', '-t', dest='print_body',
action='store_false', default=True,
help=('''
output_options = parser.add_mutually_exclusive_group(required=False)
output_options.add_argument('--print', '-p', dest='output_options',
help=_('''
String specifying what the output should 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.
The default behaviour is "hb" (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 body is printed by default.
'''.format(
request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,
))
)
output_options.add_argument(
'--verbose', '-v', dest='output_options',
action='store_const', const=''.join(OUTPUT_OPTIONS),
help=_('''
Print the whole request as well as the response.
Shortcut for --print={0}.
'''.format(''.join(OUTPUT_OPTIONS)))
)
output_options.add_argument(
'--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))
)
only.add_argument(
'--body', '-b', dest='print_headers',
action='store_false', default=True,
help=('''
output_options.add_argument(
'--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,
choices=AVAILABLE_STYLES,
help=_('''
Output coloring style, one of %s. Defaults to solarized.
''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
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(AVAILABLE_STYLES))
)
parser.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 --allow-redirects
hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected.
''')
)
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a', help='username:password',
type=KeyValueType(SEP_COMMON)
)
parser.add_argument(
'--verify',
'--auth', '-a',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_('''
Set to "yes" to check the host\'s SSL certificate.
username:password.
If only the username is provided (-a username),
HTTPie will prompt for the password.
'''),
)
parser.add_argument(
'--auth-type', choices=['basic', 'digest'], default='basic',
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=KeyValueType(SEP_COMMON),
type=KeyValueArgType(SEP_PROXY),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
@ -176,11 +192,6 @@ parser.add_argument(
(e.g. re-POST-ing of data at new ``Location``)
''')
)
parser.add_argument(
'--file', metavar='PATH', type=argparse.FileType(),
default=[], action='append',
help='File to multipart upload'
)
parser.add_argument(
'--timeout', type=float,
help=_('''
@ -195,23 +206,35 @@ parser.add_argument(
parser.add_argument(
'method', metavar='METHOD',
nargs='?',
default=None,
help=_('''
HTTP method to be used for the request
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie
will guess the HTTP method. If there is some
data to be sent, then it will be POST, otherwise GET.
''')
)
parser.add_argument(
'url', metavar='URL',
help=_('''
Protocol defaults to http:// if the
URL does not include it.
The protocol defaults to http:// if the
URL does not include one.
''')
)
parser.add_argument(
'items', nargs='*',
type=KeyValueType(SEP_COMMON, SEP_DATA, SEP_DATA_RAW_JSON),
metavar='ITEM',
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=_('''
HTTP header (key:value), data field (key=value)
or raw JSON field (field:=value).
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),
a query parameter (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.
''')
)

171
httpie/core.py Normal file
View File

@ -0,0 +1,171 @@
"""This module provides the main functionality of HTTPie.
Invocation flow:
1. Read, validate and process the input (args, `stdin`).
2. Create a request and send it, get the response.
3. Process and format the requested parts of the request-response exchange.
4. Write to `stdout` and exit.
"""
import sys
import json
import requests
import requests.auth
from requests.compat import str
from .models import HTTPMessage, Environment
from .output import OutputProcessor
from .input import (PRETTIFY_STDOUT_TTY_ONLY,
OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY)
from .cli import parser
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
TYPE_JSON = 'application/json; charset=utf-8'
def get_response(args, env):
"""Send the request and return a `request.Response`."""
auto_json = args.data and not args.form
if args.json or auto_json:
if 'Content-Type' not in args.headers:
args.headers['Content-Type'] = TYPE_JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = TYPE_FORM
try:
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.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,
params=args.params,
)
except (KeyboardInterrupt, SystemExit):
env.stderr.write('\n')
sys.exit(1)
except Exception as e:
if args.traceback:
raise
env.stderr.write(str(e.message) + '\n')
sys.exit(1)
def get_output(args, env, request, response):
"""Format parts of the `request`-`response` exchange
according to `args` and `env` and return a `unicode`.
"""
do_prettify = (args.prettify is True
or (args.prettify == PRETTIFY_STDOUT_TTY_ONLY
and env.stdout_isatty))
do_output_request = (OUT_REQ_HEAD in args.output_options
or OUT_REQ_BODY in args.output_options)
do_output_response = (OUT_RESP_HEAD in args.output_options
or OUT_RESP_BODY in args.output_options)
prettifier = None
if do_prettify:
prettifier = OutputProcessor(
env, pygments_style=args.style)
buf = []
if do_output_request:
req_msg = HTTPMessage.from_request(request)
req = req_msg.format(
prettifier=prettifier,
with_headers=OUT_REQ_HEAD in args.output_options,
with_body=OUT_REQ_BODY in args.output_options
)
buf.append(req)
buf.append('\n')
if do_output_response:
buf.append('\n')
if do_output_response:
resp_msg = HTTPMessage.from_response(response)
resp = resp_msg.format(
prettifier=prettifier,
with_headers=OUT_RESP_HEAD in args.output_options,
with_body=OUT_RESP_BODY in args.output_options
)
buf.append(resp)
buf.append('\n')
return ''.join(buf)
def get_exist_status(code, allow_redirects=False):
"""Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not allow_redirects:
# Redirect
return 3
elif 400 <= code <= 499:
# Client Error
return 4
elif 500 <= code <= 599:
# Server Error
return 5
else:
return 0
def main(args=sys.argv[1:], env=Environment()):
"""Run the main program and write the output to ``env.stdout``.
Return exit status.
"""
args = parser.parse_args(args=args, env=env)
response = get_response(args, env)
status = 0
if args.check_status:
status = get_exist_status(response.status_code,
args.allow_redirects)
if status and not env.stdout_isatty:
err = 'http error: %s %s\n' % (
response.raw.status, response.raw.reason)
env.stderr.write(err.encode('utf8'))
output = get_output(args, env, response.request, response)
output_bytes = output.encode('utf8')
f = getattr(env.stdout, 'buffer', env.stdout)
f.write(output_bytes)
return status

430
httpie/input.py Normal file
View File

@ -0,0 +1,430 @@
"""Parsing and processing of CLI input (args, auth credentials, files, stdin).
"""
import os
import sys
import re
import json
import argparse
import mimetypes
import getpass
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
from requests.compat import str
from . import __version__
HTTP_POST = 'POST'
HTTP_GET = 'GET'
# Various separators used in args
SEP_HEADERS = ':'
SEP_CREDENTIALS = ':'
SEP_PROXY = ':'
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
SEP_QUERY = '=='
# Separators that become request data
SEP_GROUP_DATA_ITEMS = frozenset([
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES
])
# Separators allowed in ITEM arguments
SEP_GROUP_ITEMS = frozenset([
SEP_HEADERS,
SEP_QUERY,
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES
])
# 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
])
# Defaults
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class Parser(argparse.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(Parser, self).__init__(*args, **kwargs)
# Help only as --help (-h is used for --headers).
self.add_argument('--help',
action='help', default=argparse.SUPPRESS,
help=argparse._('show this help message and exit'))
#noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None):
args = super(Parser, self).parse_args(args, namespace)
self._process_output_options(args, env)
self._guess_method(args, env)
self._parse_items(args)
if not env.stdin_isatty:
self._body_from_file(args, env.stdin)
if args.auth and not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password()
return args
def _body_from_file(self, args, f):
"""Use the content of `f` as the `request.data`.
There can only be one source of request data.
"""
if args.data:
self.error('Request body (from stdin or a file) and request '
'data (key=value) cannot be mixed.')
args.data = f.read()
def _guess_method(self, args, env):
"""Set `args.method` if not specified to either POST or GET
based on whether the request has data or not.
"""
if args.method is None:
# Invoked as `http URL'.
assert not args.items
if not env.stdin_isatty:
args.method = HTTP_POST
else:
args.method = HTTP_GET
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
elif not re.match('^[a-zA-Z]+$', 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.
args.items.insert(
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
except argparse.ArgumentTypeError as e:
if args.traceback:
raise
self.error(e.message)
else:
# Set the URL correctly
args.url = args.method
# Infer the method
has_data = not env.stdin_isatty or any(
item.sep in SEP_GROUP_DATA_ITEMS for item in args.items)
args.method = HTTP_POST if has_data else HTTP_GET
def _parse_items(self, args):
"""Parse `args.items` into `args.headers`, `args.data`,
`args.`, and `args.files`.
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = ParamDict() if args.form else OrderedDict()
args.files = OrderedDict()
args.params = ParamDict()
try:
parse_items(items=args.items,
headers=args.headers,
data=args.data,
files=args.files,
params=args.params)
except ParseError as e:
if args.traceback:
raise
self.error(e.message)
if args.files and not args.form:
# `http url @/path/to/file`
# It's not --form so the file contents will be used as the
# body of the requests. Also, we try to detect the appropriate
# Content-Type.
if len(args.files) > 1:
self.error(
'Only one file can be specified unless'
' --form is used. File fields: %s'
% ','.join(args.files.keys()))
f = list(args.files.values())[0]
self._body_from_file(args, f)
# Reset files
args.files = {}
if 'Content-Type' not in args.headers:
mime, encoding = mimetypes.guess_type(f.name, strict=False)
if mime:
content_type = mime
if encoding:
content_type = '%s; charset=%s' % (mime, encoding)
args.headers['Content-Type'] = content_type
def _process_output_options(self, args, env):
"""Apply defaults to output options or validate the provided ones.
The default output options are stdout-type-sensitive.
"""
if not args.output_options:
args.output_options = (OUTPUT_OPTIONS_DEFAULT if env.stdout_isatty
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED)
unknown = set(args.output_options) - OUTPUT_OPTIONS
if unknown:
self.error('Unknown output options: %s' % ','.join(unknown))
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__
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
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(s):
"""Tokenize `s`. There are only two token types - strings
and escaped characters:
>>> tokenize(r'foo\=bar\\baz')
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
"""
tokens = ['']
esc = False
for c in s:
if esc:
tokens.extend([Escaped(c), ''])
esc = False
else:
if c == '\\':
esc = True
else:
tokens[-1] += c
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 argparse.ArgumentTypeError(
'"%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(prompt)
def has_password(self):
return self.value is not None
def prompt_password(self):
try:
self.value = self._getpass("Password for user '%s': " % self.key)
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 argparse.ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
key=string,
value=None,
sep=SEP_CREDENTIALS,
orig=string
)
class ParamDict(OrderedDict):
"""Multi-value dict for URL parameters and form data."""
#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.
"""
# NOTE: Won't work when used for form data with multiple values
# for a field and a file field is present:
# https://github.com/kennethreitz/requests/issues/737
if key not in self:
super(ParamDict, self).__setitem__(key, value)
else:
if not isinstance(self[key], list):
super(ParamDict, self).__setitem__(key, [self[key]])
self[key].append(value)
def parse_items(items, data=None, headers=None, files=None, params=None):
"""Parse `KeyValue` `items` into `data`, `headers`, `files`,
and `params`.
"""
if headers is None:
headers = CaseInsensitiveDict()
if data is None:
data = OrderedDict()
if files is None:
files = OrderedDict()
if params is None:
params = ParamDict()
for item in items:
value = item.value
key = item.key
if item.sep == SEP_HEADERS:
target = headers
elif item.sep == SEP_QUERY:
target = params
elif item.sep == SEP_FILES:
try:
value = open(os.path.expanduser(item.value), 'r')
except IOError as e:
raise ParseError(
'Invalid argument "%s": %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 TypeError(item)
target[key] = value
return headers, data, files, params

139
httpie/models.py Normal file
View File

@ -0,0 +1,139 @@
import os
import sys
from requests.compat import urlparse, is_windows
class Environment(object):
"""Holds information about the execution context.
Groups various aspects of the environment in a changeable object
and allows for mocking.
"""
stdin_isatty = sys.stdin.isatty()
stdin = sys.stdin
if is_windows:
# `colorama` patches `sys.stdout` so its initialization
# needs to happen before the default environment is set.
import colorama
colorama.init()
del colorama
stdout_isatty = sys.stdout.isatty()
stdout = sys.stdout
stderr = sys.stderr
# Can be set to 0 to disable colors completely.
colors = 256 if '256color' in os.environ.get('TERM', '') else 88
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
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 format(self, prettifier=None, with_headers=True, with_body=True):
"""Return a `unicode` representation of `self`. """
pretty = prettifier is not None
bits = []
if with_headers:
bits.append(self.line)
bits.append(self.headers)
if pretty:
bits = [
prettifier.process_headers('\n'.join(bits))
]
if with_body and self.body:
bits.append('\n')
if with_body and self.body:
if pretty and self.content_type:
bits.append(prettifier.process_body(
self.body, self.content_type))
else:
bits.append(self.body)
return '\n'.join(bit.strip() for bit in bits)
@staticmethod
def from_request(request):
"""Make an `HTTPMessage` from `requests.models.Request`."""
url = urlparse(request.url)
# Querystring
qs = ''
if url.query or request.params:
qs = '?'
if url.query:
qs += url.query
# Requests doesn't make params part of ``request.url``.
if request.params:
if url.query:
qs += '&'
#noinspection PyUnresolvedReferences
qs += type(request)._encode_params(request.params)
# Request-Line
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=request.method,
path=url.path or '/',
query=qs
)
# Headers
headers = dict(request.headers)
content_type = headers.get('Content-Type')
if 'Host' not in headers:
headers['Host'] = url.netloc
headers = '\n'.join(
str('%s: %s') % (name, value)
for name, value
in headers.items()
)
# Body
try:
body = request.data
except AttributeError:
# requests < 0.12.1
body = request._enc_data
if isinstance(body, dict):
#noinspection PyUnresolvedReferences
body = type(request)._encode_params(body)
return HTTPMessage(
line=request_line,
headers=headers,
body=body,
content_type=content_type
)
@classmethod
def from_response(cls, response):
"""Make an `HTTPMessage` from `requests.models.Response`."""
encoding = response.encoding or 'ISO-8859-1'
original = response.raw._original_response
response_headers = response.headers
status_line = 'HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status,
reason=original.reason
)
body = response.content.decode(encoding) if response.content else ''
return cls(
line=status_line,
headers=str(original.msg),
body=body,
content_type=response_headers.get('Content-Type'))

176
httpie/output.py Normal file
View File

@ -0,0 +1,176 @@
"""Output processing and formatting.
"""
import re
import json
import pygments
from pygments import token, lexer
from pygments.styles import get_style_by_name, STYLE_MAP
from pygments.lexers import get_lexer_for_mimetype
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound
from requests.compat import is_windows
from . import solarized
DEFAULT_STYLE = 'solarized'
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
class HTTPLexer(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
(`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+)',
lexer.bygroups(
token.Name.Function,
token.Text,
token.Name.Namespace,
token.Text,
token.Keyword.Reserved,
token.Operator,
token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
lexer.bygroups(
token.Keyword.Reserved, # 'HTTP'
token.Operator, # '/'
token.Number, # Version
token.Text,
token.Number, # Status code
token.Text,
token.Name.Exception, # Reason
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
token.Name.Attribute, # Name
token.Text,
token.Operator, # Colon
token.Text,
token.String # Value
))
]}
class BaseProcessor(object):
enabled = True
def __init__(self, env, **kwargs):
self.env = env
self.kwargs = kwargs
def process_headers(self, headers):
return headers
def process_body(self, content, content_type):
return content
class JSONProcessor(BaseProcessor):
def process_body(self, content, content_type):
if content_type == 'application/json':
try:
# Indent and sort the JSON data.
content = json.dumps(
json.loads(content),
sort_keys=True,
ensure_ascii=False,
indent=4,
)
except ValueError:
# Invalid JSON - we don't care.
pass
return content
class PygmentsProcessor(BaseProcessor):
def __init__(self, *args, **kwargs):
super(PygmentsProcessor, self).__init__(*args, **kwargs)
if not self.env.colors:
self.enabled = False
return
try:
style = get_style_by_name(
self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound:
style = solarized.SolarizedStyle
if is_windows or self.env.colors == 256:
fmt_class = Terminal256Formatter
else:
fmt_class = TerminalFormatter
self.formatter = fmt_class(style=style)
def process_headers(self, headers):
return pygments.highlight(
headers, HTTPLexer(), self.formatter)
def process_body(self, content, content_type):
try:
lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
pass
else:
content = pygments.highlight(content, lexer, self.formatter)
return content
class OutputProcessor(object):
installed_processors = [
JSONProcessor,
PygmentsProcessor
]
def __init__(self, env, **kwargs):
self.env = env
processors = [
cls(env, **kwargs)
for cls in self.installed_processors
]
self.processors = [p for p in processors if p.enabled]
def process_headers(self, headers):
for processor in self.processors:
headers = processor.process_headers(headers)
return headers
def process_body(self, content, content_type):
content_type = content_type.split(';')[0]
application_match = re.match(
r'application/(.+\+)(json|xml)$',
content_type
)
if application_match:
# Strip vendor and extensions from Content-Type
vendor, extension = application_match.groups()
content_type = content_type.replace(vendor, '')
for processor in self.processors:
content = processor.process_body(content, content_type)
return content

View File

@ -1,62 +0,0 @@
import os
import json
import pygments
from pygments import token
from pygments.util import ClassNotFound
from pygments.lexers import get_lexer_for_mimetype
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.formatters.terminal import TerminalFormatter
from pygments.lexer import RegexLexer, bygroups
from pygments.styles import get_style_by_name, STYLE_MAP
from . import solarized
DEFAULT_STYLE = 'solarized'
AVAILABLE_STYLES = [DEFAULT_STYLE] + STYLE_MAP.keys()
TYPE_JS = 'application/javascript'
FORMATTER = (Terminal256Formatter
if os.environ.get('TERM') == 'xterm-256color'
else TerminalFormatter)
class HTTPLexer(RegexLexer):
name = 'HTTP'
aliases = ['http']
filenames = ['*.http']
tokens = {
'root': [
(r'\s+', token.Text),
(r'(HTTP/[\d.]+\s+)(\d+)(\s+.+)', bygroups(
token.Operator, token.Number, token.String)),
(r'(.*?:)(.+)', bygroups(token.Name, token.String))
]}
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]
if 'json' in content_type:
content_type = TYPE_JS
try:
# Indent JSON
content = json.dumps(json.loads(content),
sort_keys=True, indent=4)
except Exception:
pass
try:
lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
return content
content = pygments.highlight(content, lexer, self.formatter)
return content

View File

@ -27,7 +27,8 @@ THE SOFTWARE.
"""
from pygments.style import Style
from pygments.token import Token, Comment, Name, Keyword, Generic, Number, Operator, String
from pygments.token import (Token, Comment, Name, Keyword, Generic, Number,
Operator, String)
BASE03 = '#002B36'

View File

@ -5,19 +5,28 @@ import httpie
if sys.argv[-1] == 'test':
os.system('python tests.py')
sys.exit()
status = os.system('python tests/tests.py')
sys.exit(1 if status > 127 else status)
requirements = ['requests>=0.10.4', 'Pygments>=1.4']
if sys.version_info < (2, 7):
requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'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')
if 'win32' in str(sys.platform).lower():
# Terminal colors for Windows
requirements.append('colorama>=0.2.4')
setup(
name='httpie',version=httpie.__version__,
name='httpie',
version=httpie.__version__,
description=httpie.__doc__.strip(),
long_description=open('README.md').read(),
long_description=open('README.rst').read(),
url='http://httpie.org/',
download_url='https://github.com/jkbr/httpie',
author=httpie.__author__,
@ -35,10 +44,8 @@ setup(
'Programming Language :: Python',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
# TODO: Python 3
# 'Programming Language :: Python :: 3.1'
# 'Programming Language :: Python :: 3.2'
# 'Programming Language :: Python :: 3.3'
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
@ -48,5 +55,6 @@ setup(
'Topic :: System :: Networking',
'Topic :: Terminals',
'Topic :: Text Processing',
]
'Topic :: Utilities'
],
)

115
tests.py
View File

@ -1,115 +0,0 @@
import sys
import unittest
import argparse
from StringIO import StringIO
from httpie import __main__
from httpie import cli
TERMINAL_COLOR_CHECK = '\x1b['
def http(*args, **kwargs):
http_kwargs = {
'stdin_isatty': True,
'stdout_isatty': False
}
http_kwargs.update(kwargs)
stdout = http_kwargs.setdefault('stdout', StringIO())
__main__.main(args=args, **http_kwargs)
return stdout.getvalue()
class BaseTest(unittest.TestCase):
if sys.version < (2, 7):
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.kv = cli.KeyValueType(
cli.SEP_HEADERS,
cli.SEP_DATA,
cli.SEP_DATA_RAW_JSON
)
def test_invalid_items(self):
items = ['no-separator']
for item in items:
self.assertRaises(argparse.ArgumentTypeError,
lambda: self.kv(item))
def test_valid_items(self):
headers, data = cli.parse_items([
self.kv('string=value'),
self.kv('header:value'),
self.kv('list:=["a", 1, {}, false]'),
self.kv('obj:={"a": "b"}'),
self.kv('eh:'),
self.kv('ed='),
self.kv('bool:=true'),
])
self.assertDictEqual(headers, {
'header': 'value',
'eh': ''
})
self.assertDictEqual(data, {
"ed": "",
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"}
})
class TestHTTPie(BaseTest):
def test_get(self):
http('GET', 'http://httpbin.org/get')
def test_json(self):
response = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
def test_form(self):
response = http('POST', '--form', '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_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_CHECK, r)
def test_force_pretty(self):
r = http('GET', '--pretty', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_CHECK, r)
def test_force_ugly(self):
r = http('GET', '--ugly', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_CHECK, r)
if __name__ == '__main__':
unittest.main()

1
tests/file.txt Normal file
View File

@ -0,0 +1 @@
__test_file_content__

1
tests/file2.txt Normal file
View File

@ -0,0 +1 @@
__test_file_content__

891
tests/tests.py Executable file
View File

@ -0,0 +1,891 @@
#!/usr/bin/env python
# coding=utf8
"""
Many of the test cases here use httpbin.org.
To make it run faster and offline you can::
# Install `httpbin` locally
pip install git+https://github.com/kennethreitz/httpbin.git
# Run it
httpbin
# Run the tests against it
HTTPBIN_URL=http://localhost:5000 python setup.py test
# Test all Python environments
HTTPBIN_URL=http://localhost:5000 tox
"""
import os
import sys
import json
import tempfile
import unittest
import argparse
import requests
from requests.compat import is_py26, is_py3, str
#################################################################
# Utils/setup
#################################################################
# HACK: Prepend ../ to PYTHONPATH so that we can import httpie form there.
TESTS_ROOT = os.path.dirname(__file__)
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import input
from httpie.models import Environment
from httpie.core import main, get_output
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
'http://httpbin.org')
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
def httpbin(path):
return HTTPBIN_URL + path
class Response(str):
"""
A unicode subclass holding the output of `main()`, and also
the exit status, the contents of ``stderr``, and de-serialized
JSON response (if possible).
"""
exit_status = None
stderr = None
json = None
def http(*args, **kwargs):
"""
Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a unicode response.
"""
if 'env' not in kwargs:
# Ensure that we have terminal by default (needed for Travis).
kwargs['env'] = Environment(
colors=0,
stdin_isatty=True,
stdout_isatty=True,
)
stdout = kwargs['env'].stdout = tempfile.TemporaryFile()
stderr = kwargs['env'].stderr = tempfile.TemporaryFile()
exit_status = main(args=['--traceback'] + list(args), **kwargs)
stdout.seek(0)
stderr.seek(0)
r = Response(stdout.read().decode('utf8'))
r.stderr = stderr.read().decode('utf8')
r.exit_status = exit_status
stdout.close()
stderr.close()
if TERMINAL_COLOR_PRESENCE_CHECK not in r:
# De-serialize JSON body if possible.
if r.strip().startswith('{'):
#noinspection PyTypeChecker
r.json = json.loads(r)
elif r.count('Content-Type:') == 1 and 'application/json' in r:
try:
j = r.strip()[r.strip().rindex('\n\n'):]
except ValueError:
pass
else:
try:
r.json = json.loads(j)
except ValueError:
pass
return r
class BaseTestCase(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)
#################################################################
# High-level tests using httpbin.
#################################################################
class HTTPieTest(BaseTestCase):
def test_GET(self):
r = http(
'GET',
httpbin('/get')
)
self.assertIn('HTTP/1.1 200', r)
def test_DELETE(self):
r = http(
'DELETE',
httpbin('/delete')
)
self.assertIn('HTTP/1.1 200', r)
def test_PUT(self):
r = http(
'PUT',
httpbin('/put'),
'foo=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_JSON_data(self):
r = http(
'POST',
httpbin('/post'),
'foo=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_form(self):
r = http(
'--form',
'POST',
httpbin('/post'),
'foo=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_form_multiple_values(self):
r = http(
'--form',
'POST',
httpbin('/post'),
'foo=bar',
'foo=baz',
)
self.assertIn('HTTP/1.1 200', r)
self.assertDictEqual(r.json['form'], {
'foo': ['bar', 'baz']
})
def test_POST_stdin(self):
env = Environment(
stdin=open(TEST_FILE_PATH),
stdin_isatty=False,
stdout_isatty=True,
colors=0,
)
r = http(
'--form',
'POST',
httpbin('/post'),
env=env
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
def test_headers(self):
r = http(
'GET',
httpbin('/headers'),
'Foo:bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"User-Agent": "HTTPie', r)
self.assertIn('"Foo": "bar"', r)
class QuerystringTest(BaseTestCase):
def test_query_string_params_in_url(self):
r = http(
'--print=Hhb',
'GET',
httpbin('/get?a=1&b=2')
)
path = '/get?a=1&b=2'
url = httpbin(path)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r)
def test_query_string_params_items(self):
r = http(
'--print=Hhb',
'GET',
httpbin('/get'),
'a==1',
'b==2'
)
path = '/get?a=1&b=2'
url = httpbin(path)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r)
def test_query_string_params_in_url_and_items_with_duplicates(self):
r = http(
'--print=Hhb',
'GET',
httpbin('/get?a=1&a=1'),
'a==1',
'a==1',
'b==2',
)
path = '/get?a=1&a=1&a=1&a=1&b=2'
url = httpbin(path)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r)
class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
"""
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):
# https://github.com/jkbr/httpie/issues/62
r = http(
'GET',
httpbin('/headers')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "*/*"', r)
self.assertNotIn('"Content-Type": "application/json', r)
def test_POST_no_data_no_auto_headers(self):
# JSON headers shouldn't be automatically set for POST with no data.
r = http(
'POST',
httpbin('/post')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "*/*"', r)
self.assertNotIn('"Content-Type": "application/json', r)
def test_POST_with_data_auto_JSON_headers(self):
r = http(
'POST',
httpbin('/post'),
'a=b'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_GET_with_data_auto_JSON_headers(self):
# JSON headers should automatically be set also for GET with data.
r = http(
'POST',
httpbin('/post'),
'a=b'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_POST_explicit_JSON_auto_JSON_headers(self):
r = http(
'--json',
'POST',
httpbin('/post')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
def test_GET_explicit_JSON_explicit_headers(self):
r = http(
'--json',
'GET',
httpbin('/headers'),
'Accept:application/xml',
'Content-Type:application/xml'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/xml"', r)
self.assertIn('"Content-Type": "application/xml"', r)
def test_POST_form_auto_Content_Type(self):
r = http(
'--form',
'POST',
httpbin('/post')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(
'"Content-Type":'
' "application/x-www-form-urlencoded; charset=utf-8"',
r
)
def test_POST_form_Content_Type_override(self):
r = http(
'--form',
'POST',
httpbin('/post'),
'Content-Type:application/xml'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Content-Type": "application/xml"', r)
def test_print_only_body_when_stdout_redirected_by_default(self):
r = http(
'GET',
httpbin('/get'),
env=Environment(
stdin_isatty=True,
stdout_isatty=False
)
)
self.assertNotIn('HTTP/', r)
def test_print_overridable_when_stdout_redirected(self):
r = http(
'--print=h',
'GET',
httpbin('/get'),
env=Environment(
stdin_isatty=True,
stdout_isatty=False
)
)
self.assertIn('HTTP/1.1 200', r)
class ImplicitHTTPMethodTest(BaseTestCase):
def test_implicit_GET(self):
r = http(httpbin('/get'))
self.assertIn('HTTP/1.1 200', r)
def test_implicit_GET_with_headers(self):
r = http(
httpbin('/headers'),
'Foo:bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Foo": "bar"', r)
def test_implicit_POST_json(self):
r = http(
httpbin('/post'),
'hello=world'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"hello": "world"', r)
def test_implicit_POST_form(self):
r = http(
'--form',
httpbin('/post'),
'foo=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_implicit_POST_stdin(self):
env = Environment(
stdin_isatty=False,
stdin=open(TEST_FILE_PATH),
stdout_isatty=True,
colors=0,
)
r = http(
'--form',
httpbin('/post'),
env=env
)
self.assertIn('HTTP/1.1 200', r)
class PrettyFlagTest(BaseTestCase):
"""Test the --pretty / --ugly flag handling."""
def test_pretty_enabled_by_default(self):
r = http(
'GET',
httpbin('/get'),
env=Environment(
stdin_isatty=True,
stdout_isatty=True,
),
)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_pretty_enabled_by_default_unless_stdout_redirected(self):
r = http(
'GET',
httpbin('/get')
)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_pretty(self):
r = http(
'--pretty',
'GET',
httpbin('/get'),
env=Environment(
stdin_isatty=True,
stdout_isatty=False
),
)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_ugly(self):
r = http(
'--ugly',
'GET',
httpbin('/get'),
)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class VerboseFlagTest(BaseTestCase):
def test_verbose(self):
r = http(
'--verbose',
'GET',
httpbin('/get'),
'test-header:__test__'
)
self.assertIn('HTTP/1.1 200', r)
self.assertEqual(r.count('__test__'), 2)
def test_verbose_form(self):
# https://github.com/jkbr/httpie/issues/53
r = http(
'--verbose',
'--form',
'POST',
httpbin('/post'),
'foo=bar',
'baz=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('foo=bar&baz=bar', r)
class MultipartFormDataFileUploadTest(BaseTestCase):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(input.ParseError, http,
'--form',
'--traceback',
'POST',
httpbin('/post'),
'foo@/__does_not_exist__'
)
def test_upload_ok(self):
r = http(
'--form',
'POST',
httpbin('/post'),
'test-file@%s' % TEST_FILE_PATH,
'foo=bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
self.assertIn('"foo": "bar"', r)
class RequestBodyFromFilePathTest(BaseTestCase):
"""
`http URL @file'
"""
def test_request_body_from_file_by_path(self):
r = http(
'POST',
httpbin('/post'),
'@' + TEST_FILE_PATH
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
self.assertIn('"Content-Type": "text/plain"', r)
def test_request_body_from_file_by_path_with_explicit_content_type(self):
r = http(
'POST',
httpbin('/post'),
'@' + TEST_FILE_PATH,
'Content-Type:x-foo/bar'
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn(TEST_FILE_CONTENT, r)
self.assertIn('"Content-Type": "x-foo/bar"', r)
def test_request_body_from_file_by_path_only_one_file_allowed(self):
self.assertRaises(SystemExit, lambda: http(
'POST',
httpbin('/post'),
'@' + TEST_FILE_PATH,
'@' + TEST_FILE2_PATH)
)
def test_request_body_from_file_by_path_no_data_items_allowed(self):
self.assertRaises(SystemExit, lambda: http(
'POST',
httpbin('/post'),
'@' + TEST_FILE_PATH,
'foo=bar')
)
class AuthTest(BaseTestCase):
def test_basic_auth(self):
r = http(
'--auth=user:password',
'GET',
httpbin('/basic-auth/user/password')
)
self.assertIn('HTTP/1.1 200', r)
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('/digest-auth/auth/user/password')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_password_prompt(self):
input.AuthCredentials._getpass = lambda self, prompt: 'password'
r = http(
'--auth',
'user',
'GET',
httpbin('/basic-auth/user/password')
)
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
class ExitStatusTest(BaseTestCase):
def test_ok_response_exits_0(self):
r = http(
'GET',
httpbin('/status/200')
)
self.assertIn('HTTP/1.1 200', r)
self.assertEqual(r.exit_status, 0)
def test_error_response_exits_0_without_check_status(self):
r = http(
'GET',
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, 0)
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
r = http(
'--check-status',
'--headers', # non-terminal, force headers
'GET',
httpbin('/status/301'),
env=Environment(
stdout_isatty=False,
stdin_isatty=True,
)
)
self.assertIn('HTTP/1.1 301', r)
self.assertEqual(r.exit_status, 3)
self.assertIn('301 moved permanently', r.stderr.lower())
def test_3xx_check_status_redirects_allowed_exits_0(self):
r = http(
'--check-status',
'--allow-redirects',
'GET',
httpbin('/status/301')
)
# The redirect will be followed so 200 is expected.
self.assertIn('HTTP/1.1 200 OK', r)
self.assertEqual(r.exit_status, 0)
def test_4xx_check_status_exits_4(self):
r = http(
'--check-status',
'GET',
httpbin('/status/401')
)
self.assertIn('HTTP/1.1 401', r)
self.assertEqual(r.exit_status, 4)
# Also stderr should be empty since stdout isn't redirected.
self.assert_(not r.stderr)
def test_5xx_check_status_exits_5(self):
r = http(
'--check-status',
'GET',
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, 5)
#################################################################
# CLI argument parsing related tests.
#################################################################
class ItemParsingTest(BaseTestCase):
def setUp(self):
self.key_value_type = input.KeyValueArgType(
input.SEP_HEADERS,
input.SEP_QUERY,
input.SEP_DATA,
input.SEP_DATA_RAW_JSON,
input.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, params = input.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_PATH)
])
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, params = input.parse_items([
self.key_value_type('bob\\:==foo'),
])
self.assertDictEqual(params, {
'bob:': 'foo',
})
def test_valid_items(self):
headers, data, files, params = input.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_PATH),
self.key_value_type('query==value'),
])
self.assertDictEqual(headers, {
'header': 'value',
'eh': ''
})
self.assertDictEqual(data, {
"ed": "",
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"},
})
self.assertDictEqual(params, {
'query': 'value',
})
self.assertIn('test-file', files)
class ArgumentParserTestCase(unittest.TestCase):
def setUp(self):
self.parser = input.Parser()
def test_guess_when_method_set_and_valid(self):
args = argparse.Namespace()
args.method = 'GET'
args.url = 'http://example.com/'
args.items = []
self.parser._guess_method(args, Environment())
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_guess_when_method_not_set(self):
args = argparse.Namespace()
args.method = None
args.url = 'http://example.com/'
args.items = []
self.parser._guess_method(args, Environment(
stdin_isatty=True,
stdout_isatty=True,
))
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_guess_when_method_set_but_invalid_and_data_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'data=field'
args.items = []
self.parser._guess_method(args, Environment())
self.assertEquals(args.method, 'POST')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[input.KeyValue(
key='data', value='field', sep='=', orig='data=field')])
def test_guess_when_method_set_but_invalid_and_header_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'test:header'
args.items = []
self.parser._guess_method(args, Environment(
stdin_isatty=True,
stdout_isatty=True,
))
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[input.KeyValue(
key='test', value='header', sep=':', orig='test:header')])
def test_guess_when_method_set_but_invalid_and_item_exists(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'new_item=a'
args.items = [
input.KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b')
]
self.parser._guess_method(args, Environment())
self.assertEquals(args.items, [
input.KeyValue(
key='new_item', value='a', sep='=', orig='new_item=a'),
input.KeyValue(key
='old_item', value='b', sep='=', orig='old_item=b'),
])
class FakeResponse(requests.Response):
class Mock(object):
def __getattr__(self, item):
return self
def __repr__(self):
return 'Mock string'
def __unicode__(self):
return self.__repr__()
def __init__(self, content=None, encoding='utf-8'):
super(FakeResponse, self).__init__()
self.headers['Content-Type'] = 'application/json'
self.encoding = encoding
self._content = content.encode(encoding)
self.raw = self.Mock()
class UnicodeOutputTestCase(BaseTestCase):
def test_unicode_output(self):
# some cyrillic and simplified chinese symbols
response_dict = {'Привет': 'Мир!',
'Hello': '世界'}
if not is_py3:
response_dict = dict(
(k.decode('utf8'), v.decode('utf8'))
for k, v in response_dict.items()
)
response_body = json.dumps(response_dict)
# emulate response
response = FakeResponse(response_body)
# emulate cli arguments
args = argparse.Namespace()
args.prettify = True
args.output_options = 'b'
args.forced_content_type = None
args.style = 'default'
# colorized output contains escape sequences
output = get_output(args, Environment(), response.request, response)
for key, value in response_dict.items():
self.assertIn(key, output)
self.assertIn(value, output)
if __name__ == '__main__':
#noinspection PyCallingNonCallable
unittest.main()

19
tox.ini Normal file
View File

@ -0,0 +1,19 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, py32, pypy
[testenv]
commands = {envpython} setup.py test
[testenv:py26]
deps = argparse
[testenv:py30]
deps = argparse
[testenv:py31]
deps = argparse