mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 08:08:41 +02:00
Compare commits
135 Commits
Author | SHA1 | Date | |
---|---|---|---|
b53d483163 | |||
f45cc0eec0 | |||
f26f2f1438 | |||
851412c698 | |||
26a76e8243 | |||
f5cfd0143b | |||
9391c89205 | |||
76ebe7c6db | |||
7af08b6faa | |||
9944def703 | |||
728a1a195b | |||
2646ebaaed | |||
fba3912f2e | |||
0572158ba1 | |||
0a673613ef | |||
19f760450f | |||
35da44309f | |||
ced6e33230 | |||
87042f65c9 | |||
c271715a98 | |||
57fc606f6b | |||
7d82b853ae | |||
16f23d8147 | |||
ab7915d9e0 | |||
1d6fcfff73 | |||
76a3125153 | |||
24d6331d15 | |||
06ea36aaa4 | |||
c2d70e2bb1 | |||
40948dbd2e | |||
2dba176aa8 | |||
54e3e5bca4 | |||
533a662651 | |||
1ce02ebbd5 | |||
8a7f4c0d6e | |||
f29c458611 | |||
2d7df0afb4 | |||
16a7d0a719 | |||
0cffda86f6 | |||
f42ee6da85 | |||
deeb7cbbac | |||
12f2fb4a92 | |||
489bd64295 | |||
9b8cb42efd | |||
2036337a53 | |||
5ca8bec9ff | |||
df79792fd9 | |||
5a82c79fdf | |||
05b321d38f | |||
681b652bf9 | |||
85b3a016eb | |||
929ead437a | |||
36de166b28 | |||
7bc2de2f9d | |||
cb7ead04e2 | |||
cd2ca41f48 | |||
c71de95505 | |||
6ab03b21b4 | |||
50196be0f2 | |||
41d640920c | |||
3179631603 | |||
2f7921091c | |||
180313d80c | |||
926d3f5caf | |||
4613d947a8 | |||
5a47f00bac | |||
0e1affbbc4 | |||
d920f20847 | |||
bca36f0464 | |||
78fff98712 | |||
e06c448a75 | |||
9cdbd6b0ec | |||
cbc6d02127 | |||
284a75fa2f | |||
b3ea273a21 | |||
0d129d5f69 | |||
1388206f1a | |||
28dbe9f76c | |||
a0700c41ad | |||
e175fe9d0e | |||
d544ec3823 | |||
6cf2910de0 | |||
f64eb09571 | |||
126130455e | |||
70b3658004 | |||
d89eeb0796 | |||
bced559496 | |||
4aa86cb438 | |||
2d7f2c65a2 | |||
3d11042772 | |||
b8cc7c205c | |||
3a96706e18 | |||
7910269996 | |||
c5b1aaaa28 | |||
67d6426360 | |||
29e594daaf | |||
90af1f7422 | |||
16df8848e8 | |||
c29981c633 | |||
6db93b25d8 | |||
45ce446017 | |||
4da3821bc4 | |||
0c4c6c4753 | |||
ee598d304d | |||
c6c1489212 | |||
71d21d1feb | |||
153663cb92 | |||
1c5fd18465 | |||
ab23037582 | |||
3dbb61a8ca | |||
51aa0409e6 | |||
1f49900db6 | |||
e2e749b2a6 | |||
ccbea8c96e | |||
6a1f0248e1 | |||
b7e0473d6c | |||
578acacdf3 | |||
ed888a2657 | |||
5e19e1b95d | |||
02622a4135 | |||
31c28807c9 | |||
78e20c6e85 | |||
20408e12e9 | |||
3c8af4c170 | |||
c9eb2255f6 | |||
028d16d0ff | |||
f5d4addab2 | |||
ccb2aaf94f | |||
8cff0a3e67 | |||
ce952c9e90 | |||
e83e601f7c | |||
f995c61892 | |||
71771dc4a6 | |||
1517f3d149 | |||
d9abf7d31c |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ dist
|
||||
httpie.egg-info
|
||||
build
|
||||
*.pyc
|
||||
.tox
|
||||
|
10
.travis.yml
10
.travis.yml
@ -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
26
LICENSE
Normal 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
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
||||
include README.rst LICENSE
|
107
README.md
107
README.md
@ -1,107 +0,0 @@
|
||||
## HTTPie: cURL for humans
|
||||
|
||||
[](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).
|
||||
|
||||

|
||||
|
||||
### 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.6](https://github.com/jkbr/httpie/compare/0.1.4...0.1.6) (2012-03-04)
|
401
README.rst
Normal file
401
README.rst
Normal 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
|
BIN
httpie.png
BIN
httpie.png
Binary file not shown.
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 135 KiB |
@ -3,5 +3,5 @@ HTTPie - cURL for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.1.6'
|
||||
__version__ = '0.2.6'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -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())
|
||||
|
241
httpie/cli.py
241
httpie/cli.py
@ -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
171
httpie/core.py
Normal 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
430
httpie/input.py
Normal 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
139
httpie/models.py
Normal 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
176
httpie/output.py
Normal 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
|
@ -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
|
@ -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'
|
||||
|
34
setup.py
34
setup.py
@ -5,25 +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')
|
||||
|
||||
|
||||
try:
|
||||
long_description = open('README.md').read()
|
||||
except IOError:
|
||||
long_description = ''
|
||||
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=long_description,
|
||||
long_description=open('README.rst').read(),
|
||||
url='http://httpie.org/',
|
||||
download_url='https://github.com/jkbr/httpie',
|
||||
author=httpie.__author__,
|
||||
@ -41,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',
|
||||
@ -54,5 +55,6 @@ setup(
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Terminals',
|
||||
'Topic :: Text Processing',
|
||||
'Topic :: Utilities'
|
||||
],
|
||||
)
|
||||
|
115
tests.py
115
tests.py
@ -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
1
tests/file.txt
Normal file
@ -0,0 +1 @@
|
||||
__test_file_content__
|
1
tests/file2.txt
Normal file
1
tests/file2.txt
Normal file
@ -0,0 +1 @@
|
||||
__test_file_content__
|
891
tests/tests.py
Executable file
891
tests/tests.py
Executable 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
19
tox.ini
Normal 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
|
Reference in New Issue
Block a user