mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 17:54:46 +02:00
Compare commits
33 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 |
@ -3,7 +3,6 @@ python:
|
|||||||
- 2.6
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
- pypy
|
- pypy
|
||||||
- 3.1
|
|
||||||
- 3.2
|
- 3.2
|
||||||
script: python setup.py test
|
script: python setup.py test
|
||||||
install:
|
install:
|
||||||
|
296
README.rst
296
README.rst
@ -1,22 +1,33 @@
|
|||||||
|
=======================
|
||||||
HTTPie: cURL for humans
|
HTTPie: cURL for humans
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
**HTTPie is a CLI HTTP utility** built out of frustration with existing tools. The goal is to make CLI interaction with HTTP-based services as human-friendly as possible.
|
**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
|
||||||
HTTPie does so by providing an ``http`` command that allows for issuing arbitrary HTTP requests using a **simple and natural syntax** and displaying **colorized responses**:
|
**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
|
.. image:: https://github.com/jkbr/httpie/raw/master/httpie.png
|
||||||
:alt: HTTPie compared to cURL
|
:alt: HTTPie compared to cURL
|
||||||
|
|
||||||
Under the hood, HTTPie uses the excellent `Requests <http://python-requests.org>`_ and `Pygments <http://pygments.org/>`_ Python libraries. Python 2.6+ is supported (including 3.x).
|
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
|
Installation
|
||||||
------------
|
============
|
||||||
|
|
||||||
The latest **stable version** of HTTPie can always be installed (or updated to) via `pip <http://www.pip-installer.org/en/latest/index.html>`_::
|
The latest **stable version** of HTTPie can always be installed or updated
|
||||||
|
to via `pip`_ (prefered)
|
||||||
|
or ``easy_install``::
|
||||||
|
|
||||||
pip install -U httpie
|
pip install -U httpie
|
||||||
|
# easy_install pip
|
||||||
|
|
||||||
Or, you can install the **development version** directly from GitHub:
|
Or, you can install the **development version** directly from GitHub:
|
||||||
|
|
||||||
@ -28,12 +39,12 @@ Or, you can install the **development version** directly from GitHub:
|
|||||||
|
|
||||||
pip install -U https://github.com/jkbr/httpie/tarball/master
|
pip install -U https://github.com/jkbr/httpie/tarball/master
|
||||||
|
|
||||||
|
There are also packages available for `Ubuntu`_, `Debian`_ and possibly other
|
||||||
There are packages available for `Ubuntu <http://packages.ubuntu.com/quantal/httpie>`_ and `Debian <http://packages.debian.org/wheezy/httpie>`_.
|
distributions as well.
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
=====
|
||||||
|
|
||||||
Hello world::
|
Hello world::
|
||||||
|
|
||||||
@ -43,28 +54,53 @@ Synopsis::
|
|||||||
|
|
||||||
http [flags] [METHOD] URL [items]
|
http [flags] [METHOD] URL [items]
|
||||||
|
|
||||||
There are four types of key-value pair items available:
|
There are five different types of key/value pair ``items`` available:
|
||||||
|
|
||||||
Headers (``Name:Value``)
|
+-----------------------+-----------------------------------------------------+
|
||||||
Arbitrary HTTP headers. The ``:`` character is used to separate a header's name from its value, e.g., ``X-API-Token:123``.
|
| **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 |
|
||||||
|
+-----------------------+-----------------------------------------------------+
|
||||||
|
|
||||||
Simple data fields (``field=value``)
|
|
||||||
Data items are included in the request body. Depending on the ``Content-Type``, they are automatically serialized as a JSON ``Object`` (default) or ``application/x-www-form-urlencoded`` (the ``-f`` flag). Data items use ``=`` as the separator, e.g., ``hello=world``.
|
|
||||||
|
|
||||||
Raw JSON fields (``field:=value``)
|
All ``items`` come after the URL, and, unlike ``flags``, they become part of
|
||||||
This item type is needed when ``Content-Type`` is JSON and a field's value is a ``Boolean``, ``Number``, nested ``Object`` or an ``Array``, because simple data items are always serialized as ``String``. E.g. ``pies:=[1,2,3]``.
|
the actual request being is sent. Their types are distinguished by the
|
||||||
|
separator used.
|
||||||
File fields (``field@/path/to/file``)
|
|
||||||
Only available with ``-f`` / ``--form``. Use ``@`` as the separator, e.g., ``screenshot@/path/to/file.png``. The presence of a file field results into a ``multipart/form-data`` request.
|
|
||||||
|
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
^^^^^^^^
|
--------
|
||||||
::
|
::
|
||||||
|
|
||||||
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org age:=29
|
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org age:=29
|
||||||
|
|
||||||
|
|
||||||
The following request is issued::
|
The following request is issued::
|
||||||
|
|
||||||
PATCH /person/1 HTTP/1.1
|
PATCH /person/1 HTTP/1.1
|
||||||
@ -74,8 +110,8 @@ The following request is issued::
|
|||||||
|
|
||||||
{"name": "John", "email": "john@example.org", "age": 29}
|
{"name": "John", "email": "john@example.org", "age": 29}
|
||||||
|
|
||||||
|
It can easily be changed to a **form** request using the ``-f``
|
||||||
It can easily be changed to a **form** request using the ``-f`` (or ``--form``) flag, which produces::
|
(or ``--form``) flag, which produces::
|
||||||
|
|
||||||
PATCH /person/1 HTTP/1.1
|
PATCH /person/1 HTTP/1.1
|
||||||
User-Agent: HTTPie/0.1
|
User-Agent: HTTPie/0.1
|
||||||
@ -84,47 +120,88 @@ It can easily be changed to a **form** request using the ``-f`` (or ``--form``)
|
|||||||
|
|
||||||
age=29&name=John&email=john%40example.org
|
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::
|
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
|
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::
|
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">
|
<form enctype="multipart/form-data" method="post" action="http://example.com/jobs">
|
||||||
<input type="text" name="name" />
|
<input type="text" name="name" />
|
||||||
<input type="file" name="cv" />
|
<input type="file" name="cv" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
A whole request body can be passed in via **``stdin``** instead, in which case it will be used with no further processing::
|
**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
|
echo '{"name": "John"}' | http PATCH example.com/person/1 X-API-Token:123
|
||||||
# Or:
|
# Or:
|
||||||
http POST example.com/person/1 X-API-Token:123 < person.json
|
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::
|
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 -b GET https://api.github.com/repos/jkbr/httpie | http POST httpbin.org/post
|
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. The first command has no request data, whereas the second one does via ``stdin``::
|
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 -b https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
|
http https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
|
||||||
|
|
||||||
An alternative to ``stdin`` is to pass a file name 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 (using the ``mimetypes`` module). Therefore, the following will request will send the verbatim contents of the file with ``Content-Type: application/xml``::
|
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
|
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
|
Flags
|
||||||
^^^^^
|
-----
|
||||||
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
|
|
||||||
|
|
||||||
$ http --help
|
``$ http --help``::
|
||||||
usage: http [-h] [--version] [--json | --form] [--traceback]
|
|
||||||
|
usage: http [--help] [--version] [--json | --form] [--traceback]
|
||||||
[--pretty | --ugly]
|
[--pretty | --ugly]
|
||||||
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
|
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
|
||||||
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
|
[--style STYLE] [--check-status] [--auth AUTH]
|
||||||
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
|
[--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY]
|
||||||
[--timeout TIMEOUT]
|
[--allow-redirects] [--timeout TIMEOUT]
|
||||||
[METHOD] URL [ITEM [ITEM ...]]
|
[METHOD] URL [ITEM [ITEM ...]]
|
||||||
|
|
||||||
HTTPie - cURL for humans. <http://httpie.org>
|
HTTPie - cURL for humans. <http://httpie.org>
|
||||||
@ -141,12 +218,13 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
separator used. It can be an HTTP header
|
separator used. It can be an HTTP header
|
||||||
(header:value), a data field to be used in the request
|
(header:value), a data field to be used in the request
|
||||||
body (field_name=value), a raw JSON data field
|
body (field_name=value), a raw JSON data field
|
||||||
(field_name:=value), or a file field
|
(field_name:=value), a query parameter (name==value),
|
||||||
(field_name@/path/to/file). You can use a backslash to
|
or a file field (field_name@/path/to/file). You can
|
||||||
escape a colliding separator in the field name.
|
use a backslash to escape a colliding separator in the
|
||||||
|
field name.
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
--help show this help message and exit
|
||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
--json, -j (default) Data items from the command line are
|
--json, -j (default) Data items from the command line are
|
||||||
serialized as a JSON object. The Content-Type and
|
serialized as a JSON object. The Content-Type and
|
||||||
@ -164,14 +242,17 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
redirected.
|
redirected.
|
||||||
--ugly, -u Do not prettify the response.
|
--ugly, -u Do not prettify the response.
|
||||||
--print OUTPUT_OPTIONS, -p OUTPUT_OPTIONS
|
--print OUTPUT_OPTIONS, -p OUTPUT_OPTIONS
|
||||||
String specifying what should the output contain. "H"
|
String specifying what the output should contain: "H"
|
||||||
stands for the request headers and "B" for the request
|
stands for the request headers, and "B" for the
|
||||||
body. "h" stands for the response headers and "b" for
|
request body. "h" stands for the response headers and
|
||||||
response the body. Defaults to "hb" which means that
|
"b" for response the body. The default behaviour is
|
||||||
the whole response (headers and body) is printed.
|
"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.
|
--verbose, -v Print the whole request as well as the response.
|
||||||
Shortcut for --print=HBhb.
|
Shortcut for --print=HBhb.
|
||||||
--headers, -t Print only the response headers. Shortcut for
|
--headers, -h Print only the response headers. Shortcut for
|
||||||
--print=h.
|
--print=h.
|
||||||
--body, -b Print only the response body. Shortcut for --print=b.
|
--body, -b Print only the response body. Shortcut for --print=b.
|
||||||
--style STYLE, -s STYLE
|
--style STYLE, -s STYLE
|
||||||
@ -183,8 +264,18 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
make sure that the $TERM environment variable is set
|
make sure that the $TERM environment variable is set
|
||||||
to "xterm-256color" or similar (e.g., via `export TERM
|
to "xterm-256color" or similar (e.g., via `export TERM
|
||||||
=xterm-256color' in your ~/.bashrc).
|
=xterm-256color' in your ~/.bashrc).
|
||||||
--auth AUTH, -a AUTH username:password. If the password is omitted (-a
|
--check-status By default, HTTPie exits with 0 when no network or
|
||||||
username), HTTPie will prompt for it.
|
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}
|
--auth-type {basic,digest}
|
||||||
The authentication mechanism to be used. Defaults to
|
The authentication mechanism to be used. Defaults to
|
||||||
"basic".
|
"basic".
|
||||||
@ -202,19 +293,28 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
|||||||
|
|
||||||
|
|
||||||
Contribute
|
Contribute
|
||||||
-----------
|
==========
|
||||||
|
|
||||||
`View contributors on GitHub <https://github.com/jkbr/httpie/contributors>`_.
|
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.
|
||||||
|
|
||||||
If you have found a bug or have a feature request, the `issue tracker <https://github.com/jkbr/httpie/issues?state=open>`_ is the place to start a discussion about it.
|
Before working on a new feature or a bug, please browse the `existing issues`_
|
||||||
|
to see whether it has been previously discussed.
|
||||||
|
|
||||||
To contribute code or documentation, please first browse the existing issues to see if the feature/bug has previously been discussed. Then fork `the repository <https://github.com/jkbr/httpie>`_, make changes in your develop branch and submit a pull request. Note: Pull requests with tests and documentation are 53.6% more awesome :)
|
Then fork and clone `the repository`_.
|
||||||
|
|
||||||
Before a pull requests is submitted, it's a good idea to run the existing suite of tests::
|
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
|
python setup.py test
|
||||||
|
|
||||||
`Tox <http://tox.testrun.org/>`_ can used to conveniently run tests in all of the `supported Python environments <https://github.com/jkbr/httpie/blob/master/tox.ini>`_::
|
`Tox`_ can also be used to conveniently run tests in all of the
|
||||||
|
`supported Python environments`_::
|
||||||
|
|
||||||
# Install tox
|
# Install tox
|
||||||
pip install tox
|
pip install tox
|
||||||
@ -222,28 +322,80 @@ Before a pull requests is submitted, it's a good idea to run the existing suite
|
|||||||
# Run tests
|
# Run tests
|
||||||
tox
|
tox
|
||||||
|
|
||||||
Changelog
|
|
||||||
---------
|
|
||||||
|
|
||||||
* `0.2.5 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.5>`_ (2012-07-17)
|
Changelog
|
||||||
* Unicode characters in prettified JSON now don't get escaped to improve readability.
|
=========
|
||||||
|
|
||||||
|
* `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.
|
* --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``).
|
* Added support for request payloads from a file path with automatic
|
||||||
* Fixed missing query string when displaing the request headers via ``--verbose``.
|
``Content-Type`` (``http URL @/path``).
|
||||||
|
* Fixed missing query string when displaying the request headers via
|
||||||
|
``--verbose``.
|
||||||
* Fixed Content-Type for requests with no data.
|
* Fixed Content-Type for requests with no data.
|
||||||
* `0.2.2 <https://github.com/jkbr/httpie/compare/0.2.1...0.2.2>`_ (2012-06-24)
|
* `0.2.2`_ (2012-06-24)
|
||||||
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
|
* The ``METHOD`` positional argument can now be omitted (defaults to
|
||||||
|
``GET``, or to ``POST`` with data).
|
||||||
* Fixed --verbose --form.
|
* Fixed --verbose --form.
|
||||||
* Added support for `Tox <http://tox.testrun.org/>`_.
|
* Added support for `Tox`_.
|
||||||
* `0.2.1 <https://github.com/jkbr/httpie/compare/0.2.0...0.2.1>`_ (2012-06-13)
|
* `0.2.1`_ (2012-06-13)
|
||||||
* Added compatibility with ``requests-0.12.1``.
|
* Added compatibility with ``requests-0.12.1``.
|
||||||
* Dropped custom JSON and HTTP lexers in favor of the ones newly included in ``pygments-1.5``.
|
* Dropped custom JSON and HTTP lexers in favor of the ones newly included
|
||||||
* `0.2.0 <https://github.com/jkbr/httpie/compare/0.1.6...0.2.0>`_ (2012-04-25)
|
in ``pygments-1.5``.
|
||||||
|
* `0.2.0`_ (2012-04-25)
|
||||||
* Added Python 3 support.
|
* Added Python 3 support.
|
||||||
* Added the ability to print the HTTP request as well as the response (see ``--print`` and ``--verbose``).
|
* Added the ability to print the HTTP request as well as the response
|
||||||
|
(see ``--print`` and ``--verbose``).
|
||||||
* Added support for Digest authentication.
|
* Added support for Digest authentication.
|
||||||
* Added file upload support (``http -f POST file_field_name@/path/to/file``).
|
* Added file upload support
|
||||||
|
(``http -f POST file_field_name@/path/to/file``).
|
||||||
* Improved syntax highlighting for JSON.
|
* Improved syntax highlighting for JSON.
|
||||||
* Added support for field name escaping.
|
* Added support for field name escaping.
|
||||||
* Many bug fixes.
|
* Many bug fixes.
|
||||||
* `0.1.6 <https://github.com/jkbr/httpie/compare/0.1.4...0.1.6>`_ (2012-03-04)
|
* `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
|
||||||
|
@ -3,5 +3,5 @@ HTTPie - cURL for humans.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.2.5'
|
__version__ = '0.2.6'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
@ -1,130 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
"""The main entry point. Invoke as `http' or `python -m httpie'.
|
||||||
|
|
||||||
|
"""
|
||||||
import sys
|
import sys
|
||||||
import json
|
from .core import main
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from requests.compat import str
|
|
||||||
|
|
||||||
from . import httpmessage
|
|
||||||
from . import cliparse
|
|
||||||
from . import cli
|
|
||||||
from . import pretty
|
|
||||||
|
|
||||||
|
|
||||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
|
||||||
TYPE_JSON = 'application/json; charset=utf-8'
|
|
||||||
|
|
||||||
|
|
||||||
def _get_response(args):
|
|
||||||
|
|
||||||
auto_json = args.data and not args.form
|
|
||||||
if args.json or auto_json:
|
|
||||||
# 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:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Fire the request.
|
|
||||||
try:
|
|
||||||
credentials = None
|
|
||||||
if args.auth:
|
|
||||||
auth_type = (requests.auth.HTTPDigestAuth
|
|
||||||
if args.auth_type == 'digest'
|
|
||||||
else requests.auth.HTTPBasicAuth)
|
|
||||||
credentials = auth_type(args.auth.key, args.auth.value)
|
|
||||||
|
|
||||||
return requests.request(
|
|
||||||
method=args.method.lower(),
|
|
||||||
url=args.url if '://' in args.url else 'http://%s' % args.url,
|
|
||||||
headers=args.headers,
|
|
||||||
data=args.data,
|
|
||||||
verify={'yes': True, 'no': False}.get(args.verify, args.verify),
|
|
||||||
timeout=args.timeout,
|
|
||||||
auth=credentials,
|
|
||||||
proxies=dict((p.key, p.value) for p in args.proxy),
|
|
||||||
files=args.files,
|
|
||||||
allow_redirects=args.allow_redirects,
|
|
||||||
)
|
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
sys.stderr.write('\n')
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
if args.traceback:
|
|
||||||
raise
|
|
||||||
sys.stderr.write(str(e.message) + '\n')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_output(args, stdout_isatty, response):
|
|
||||||
|
|
||||||
do_prettify = (args.prettify is True or
|
|
||||||
(args.prettify == cliparse.PRETTIFY_STDOUT_TTY_ONLY
|
|
||||||
and stdout_isatty))
|
|
||||||
|
|
||||||
do_output_request = (cliparse.OUT_REQ_HEADERS in args.output_options
|
|
||||||
or cliparse.OUT_REQ_BODY in args.output_options)
|
|
||||||
|
|
||||||
do_output_response = (cliparse.OUT_RESP_HEADERS in args.output_options
|
|
||||||
or cliparse.OUT_RESP_BODY in args.output_options)
|
|
||||||
|
|
||||||
prettifier = pretty.PrettyHttp(args.style) if do_prettify else None
|
|
||||||
output = []
|
|
||||||
|
|
||||||
if do_output_request:
|
|
||||||
output.append(httpmessage.format(
|
|
||||||
message=httpmessage.from_request(response.request),
|
|
||||||
prettifier=prettifier,
|
|
||||||
with_headers=cliparse.OUT_REQ_HEADERS in args.output_options,
|
|
||||||
with_body=cliparse.OUT_REQ_BODY in args.output_options
|
|
||||||
))
|
|
||||||
output.append('\n')
|
|
||||||
if do_output_response:
|
|
||||||
output.append('\n')
|
|
||||||
|
|
||||||
if do_output_response:
|
|
||||||
output.append(httpmessage.format(
|
|
||||||
message=httpmessage.from_response(response),
|
|
||||||
prettifier=prettifier,
|
|
||||||
with_headers=cliparse.OUT_RESP_HEADERS in args.output_options,
|
|
||||||
with_body=cliparse.OUT_RESP_BODY in args.output_options
|
|
||||||
))
|
|
||||||
output.append('\n')
|
|
||||||
|
|
||||||
return ''.join(output)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None,
|
|
||||||
stdin=sys.stdin, stdin_isatty=sys.stdin.isatty(),
|
|
||||||
stdout=sys.stdout, stdout_isatty=sys.stdout.isatty()):
|
|
||||||
parser = cli.parser
|
|
||||||
args = parser.parse_args(
|
|
||||||
args=args if args is not None else sys.argv[1:],
|
|
||||||
stdin=stdin,
|
|
||||||
stdin_isatty=stdin_isatty
|
|
||||||
)
|
|
||||||
response = _get_response(args)
|
|
||||||
output = _get_output(args, stdout_isatty, response)
|
|
||||||
output_bytes = output.encode('utf8')
|
|
||||||
f = (stdout.buffer if hasattr(stdout, 'buffer') else stdout)
|
|
||||||
f.write(output_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
sys.exit(main())
|
||||||
|
120
httpie/cli.py
120
httpie/cli.py
@ -1,20 +1,24 @@
|
|||||||
"""
|
"""CLI arguments definition.
|
||||||
CLI definition.
|
|
||||||
|
NOTE: the CLI interface may change before reaching v1.0.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from . import pretty
|
|
||||||
from . import __doc__
|
from . import __doc__
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import cliparse
|
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):
|
def _(text):
|
||||||
"""Normalize white space."""
|
"""Normalize whitespace."""
|
||||||
return ' '.join(text.strip().split())
|
return ' '.join(text.strip().split())
|
||||||
|
|
||||||
|
|
||||||
desc = '%s <http://httpie.org>'
|
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
|
||||||
parser = cliparse.Parser(description=desc % __doc__.strip(),)
|
|
||||||
parser.add_argument('--version', action='version', version=__version__)
|
parser.add_argument('--version', action='version', version=__version__)
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +29,8 @@ group_type = parser.add_mutually_exclusive_group(required=False)
|
|||||||
group_type.add_argument(
|
group_type.add_argument(
|
||||||
'--json', '-j', action='store_true',
|
'--json', '-j', action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
(default) Data items from the command line are serialized as a JSON object.
|
(default) Data items from the command
|
||||||
|
line are serialized as a JSON object.
|
||||||
The Content-Type and Accept headers
|
The Content-Type and Accept headers
|
||||||
are set to application/json (if not specified).
|
are set to application/json (if not specified).
|
||||||
''')
|
''')
|
||||||
@ -34,8 +39,10 @@ group_type.add_argument(
|
|||||||
'--form', '-f', action='store_true',
|
'--form', '-f', action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
Data items from the command line are serialized as form fields.
|
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 Content-Type is set to application/x-www-form-urlencoded
|
||||||
The presence of any file fields results into a multipart/form-data request.
|
(if not specified).
|
||||||
|
The presence of any file fields results
|
||||||
|
into a multipart/form-data request.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -53,7 +60,7 @@ parser.add_argument(
|
|||||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
prettify = parser.add_mutually_exclusive_group(required=False)
|
||||||
prettify.add_argument(
|
prettify.add_argument(
|
||||||
'--pretty', dest='prettify', action='store_true',
|
'--pretty', dest='prettify', action='store_true',
|
||||||
default=cliparse.PRETTIFY_STDOUT_TTY_ONLY,
|
default=PRETTIFY_STDOUT_TTY_ONLY,
|
||||||
help=_('''
|
help=_('''
|
||||||
If stdout is a terminal, the response is prettified
|
If stdout is a terminal, the response is prettified
|
||||||
by default (colorized and indented if it is JSON).
|
by default (colorized and indented if it is JSON).
|
||||||
@ -69,70 +76,95 @@ prettify.add_argument(
|
|||||||
|
|
||||||
output_options = parser.add_mutually_exclusive_group(required=False)
|
output_options = parser.add_mutually_exclusive_group(required=False)
|
||||||
output_options.add_argument('--print', '-p', dest='output_options',
|
output_options.add_argument('--print', '-p', dest='output_options',
|
||||||
default=cliparse.OUT_RESP_HEADERS + cliparse.OUT_RESP_BODY,
|
|
||||||
help=_('''
|
help=_('''
|
||||||
String specifying what should the output contain.
|
String specifying what the output should contain:
|
||||||
"{request_headers}" stands for the request headers and
|
"{request_headers}" stands for the request headers, and
|
||||||
"{request_body}" for the request body.
|
"{request_body}" for the request body.
|
||||||
"{response_headers}" stands for the response headers and
|
"{response_headers}" stands for the response headers and
|
||||||
"{response_body}" for response the body.
|
"{response_body}" for response the body.
|
||||||
Defaults to "hb" which means that the whole response
|
The default behaviour is "hb" (i.e., the response
|
||||||
(headers and body) is printed.
|
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(
|
'''.format(
|
||||||
request_headers=cliparse.OUT_REQ_HEADERS,
|
request_headers=OUT_REQ_HEAD,
|
||||||
request_body=cliparse.OUT_REQ_BODY,
|
request_body=OUT_REQ_BODY,
|
||||||
response_headers=cliparse.OUT_RESP_HEADERS,
|
response_headers=OUT_RESP_HEAD,
|
||||||
response_body=cliparse.OUT_RESP_BODY,
|
response_body=OUT_RESP_BODY,
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--verbose', '-v', dest='output_options',
|
'--verbose', '-v', dest='output_options',
|
||||||
action='store_const', const=''.join(cliparse.OUTPUT_OPTIONS),
|
action='store_const', const=''.join(OUTPUT_OPTIONS),
|
||||||
help=_('''
|
help=_('''
|
||||||
Print the whole request as well as the response.
|
Print the whole request as well as the response.
|
||||||
Shortcut for --print={0}.
|
Shortcut for --print={0}.
|
||||||
'''.format(''.join(cliparse.OUTPUT_OPTIONS)))
|
'''.format(''.join(OUTPUT_OPTIONS)))
|
||||||
)
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--headers', '-t', dest='output_options',
|
'--headers', '-h', dest='output_options',
|
||||||
action='store_const', const=cliparse.OUT_RESP_HEADERS,
|
action='store_const', const=OUT_RESP_HEAD,
|
||||||
help=_('''
|
help=_('''
|
||||||
Print only the response headers.
|
Print only the response headers.
|
||||||
Shortcut for --print={0}.
|
Shortcut for --print={0}.
|
||||||
'''.format(cliparse.OUT_RESP_HEADERS))
|
'''.format(OUT_RESP_HEAD))
|
||||||
)
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--body', '-b', dest='output_options',
|
'--body', '-b', dest='output_options',
|
||||||
action='store_const', const=cliparse.OUT_RESP_BODY,
|
action='store_const', const=OUT_RESP_BODY,
|
||||||
help=_('''
|
help=_('''
|
||||||
Print only the response body.
|
Print only the response body.
|
||||||
Shortcut for --print={0}.
|
Shortcut for --print={0}.
|
||||||
'''.format(cliparse.OUT_RESP_BODY))
|
'''.format(OUT_RESP_BODY))
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--style', '-s', dest='style', default='solarized', metavar='STYLE',
|
'--style', '-s', dest='style', default='solarized', metavar='STYLE',
|
||||||
choices=pretty.AVAILABLE_STYLES,
|
choices=AVAILABLE_STYLES,
|
||||||
help=_('''
|
help=_('''
|
||||||
Output coloring style, one of %s. Defaults to solarized.
|
Output coloring style, one of %s. Defaults to solarized.
|
||||||
For this option to work properly, please make sure that the
|
For this option to work properly, please make sure that the
|
||||||
$TERM environment variable is set to "xterm-256color" or similar
|
$TERM environment variable is set to "xterm-256color" or similar
|
||||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||||
''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
|
''') % ', '.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.
|
# ``requests.request`` keyword arguments.
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON),
|
'--auth', '-a',
|
||||||
|
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
||||||
help=_('''
|
help=_('''
|
||||||
username:password.
|
username:password.
|
||||||
If only the username is provided (-a username), HTTPie will prompt for the password.
|
If only the username is provided (-a username),
|
||||||
|
HTTPie will prompt for the password.
|
||||||
'''),
|
'''),
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--auth-type', choices=['basic', 'digest'],
|
'--auth-type', choices=['basic', 'digest'], default='basic',
|
||||||
help=_('The authentication mechanism to be used. Defaults to "basic".')
|
help=_('''
|
||||||
|
The authentication mechanism to be used.
|
||||||
|
Defaults to "basic".
|
||||||
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -147,7 +179,7 @@ parser.add_argument(
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--proxy', default=[], action='append',
|
'--proxy', default=[], action='append',
|
||||||
type=cliparse.KeyValueType(cliparse.SEP_COMMON),
|
type=KeyValueArgType(SEP_PROXY),
|
||||||
help=_('''
|
help=_('''
|
||||||
String mapping protocol to the URL of the proxy
|
String mapping protocol to the URL of the proxy
|
||||||
(e.g. http:foo.bar:3128).
|
(e.g. http:foo.bar:3128).
|
||||||
@ -179,8 +211,9 @@ parser.add_argument(
|
|||||||
help=_('''
|
help=_('''
|
||||||
The HTTP method to be used for the request
|
The HTTP method to be used for the request
|
||||||
(GET, POST, PUT, DELETE, PATCH, ...).
|
(GET, POST, PUT, DELETE, PATCH, ...).
|
||||||
If this argument is omitted, then HTTPie will guess the HTTP method.
|
If this argument is omitted, then HTTPie
|
||||||
If there is some data to be sent, then it will be POST, otherwise GET.
|
will guess the HTTP method. If there is some
|
||||||
|
data to be sent, then it will be POST, otherwise GET.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -193,18 +226,15 @@ parser.add_argument(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'items', nargs='*',
|
'items', nargs='*',
|
||||||
metavar='ITEM',
|
metavar='ITEM',
|
||||||
type=cliparse.KeyValueType(
|
type=KeyValueArgType(*SEP_GROUP_ITEMS),
|
||||||
cliparse.SEP_COMMON,
|
|
||||||
cliparse.SEP_DATA,
|
|
||||||
cliparse.SEP_DATA_RAW_JSON,
|
|
||||||
cliparse.SEP_FILES
|
|
||||||
),
|
|
||||||
help=_('''
|
help=_('''
|
||||||
A key-value pair whose type is defined by the separator used. It can be an
|
A key-value pair whose type is defined by the
|
||||||
HTTP header (header:value),
|
separator used. It can be an HTTP header (header:value),
|
||||||
a data field to be used in the request body (field_name=value),
|
a data field to be used in the request body (field_name=value),
|
||||||
a raw JSON data field (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).
|
or a file field (field_name@/path/to/file).
|
||||||
You can use a backslash to escape a colliding separator in the field name.
|
You can use a backslash to escape a colliding
|
||||||
|
separator in the field name.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
@ -1,301 +0,0 @@
|
|||||||
"""
|
|
||||||
CLI argument parsing logic.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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 . import __version__
|
|
||||||
|
|
||||||
|
|
||||||
SEP_COMMON = ':'
|
|
||||||
SEP_HEADERS = SEP_COMMON
|
|
||||||
SEP_DATA = '='
|
|
||||||
SEP_DATA_RAW_JSON = ':='
|
|
||||||
SEP_FILES = '@'
|
|
||||||
DATA_ITEM_SEPARATORS = [
|
|
||||||
SEP_DATA,
|
|
||||||
SEP_DATA_RAW_JSON,
|
|
||||||
SEP_FILES
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
OUT_REQ_HEADERS = 'H'
|
|
||||||
OUT_REQ_BODY = 'B'
|
|
||||||
OUT_RESP_HEADERS = 'h'
|
|
||||||
OUT_RESP_BODY = 'b'
|
|
||||||
OUTPUT_OPTIONS = [OUT_REQ_HEADERS,
|
|
||||||
OUT_REQ_BODY,
|
|
||||||
OUT_RESP_HEADERS,
|
|
||||||
OUT_RESP_BODY]
|
|
||||||
|
|
||||||
|
|
||||||
PRETTIFY_STDOUT_TTY_ONLY = object()
|
|
||||||
|
|
||||||
DEFAULT_UA = 'HTTPie/%s' % __version__
|
|
||||||
|
|
||||||
|
|
||||||
class Parser(argparse.ArgumentParser):
|
|
||||||
|
|
||||||
def parse_args(self, args=None, namespace=None,
|
|
||||||
stdin=sys.stdin,
|
|
||||||
stdin_isatty=sys.stdin.isatty()):
|
|
||||||
|
|
||||||
args = super(Parser, self).parse_args(args, namespace)
|
|
||||||
|
|
||||||
self._validate_output_options(args)
|
|
||||||
self._validate_auth_options(args)
|
|
||||||
self._guess_method(args, stdin_isatty)
|
|
||||||
self._parse_items(args)
|
|
||||||
|
|
||||||
if not stdin_isatty:
|
|
||||||
self._body_from_file(args, stdin)
|
|
||||||
|
|
||||||
if args.auth and not args.auth.has_password():
|
|
||||||
# stdin has already been read (if not a tty) so
|
|
||||||
# it's save to prompt now.
|
|
||||||
args.auth.prompt_password()
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
def _body_from_file(self, args, f):
|
|
||||||
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, stdin_isatty=sys.stdin.isatty()):
|
|
||||||
"""
|
|
||||||
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 stdin_isatty:
|
|
||||||
args.method = 'POST'
|
|
||||||
else:
|
|
||||||
args.method = '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`.
|
|
||||||
# - The first item is now in `args.url`.
|
|
||||||
#
|
|
||||||
# So we need to:
|
|
||||||
# - Guess the HTTP method.
|
|
||||||
# - Set `args.url` correctly.
|
|
||||||
# - Parse the first item and move it to `args.items[0]`.
|
|
||||||
|
|
||||||
item = KeyValueType(
|
|
||||||
SEP_COMMON,
|
|
||||||
SEP_DATA,
|
|
||||||
SEP_DATA_RAW_JSON,
|
|
||||||
SEP_FILES).__call__(args.url)
|
|
||||||
|
|
||||||
args.url = args.method
|
|
||||||
args.items.insert(0, item)
|
|
||||||
|
|
||||||
has_data = not stdin_isatty or any(
|
|
||||||
item.sep in DATA_ITEM_SEPARATORS for item in args.items)
|
|
||||||
if has_data:
|
|
||||||
args.method = 'POST'
|
|
||||||
else:
|
|
||||||
args.method = 'GET'
|
|
||||||
|
|
||||||
def _parse_items(self, args):
|
|
||||||
"""
|
|
||||||
Parse `args.items` into `args.headers`, `args.data` and `args.files`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
args.headers = CaseInsensitiveDict()
|
|
||||||
args.headers['User-Agent'] = DEFAULT_UA
|
|
||||||
args.data = OrderedDict()
|
|
||||||
args.files = OrderedDict()
|
|
||||||
try:
|
|
||||||
parse_items(items=args.items, headers=args.headers,
|
|
||||||
data=args.data, files=args.files)
|
|
||||||
except ParseError as e:
|
|
||||||
if args.traceback:
|
|
||||||
raise
|
|
||||||
self.error(e.message)
|
|
||||||
|
|
||||||
if args.files and not args.form:
|
|
||||||
# `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)
|
|
||||||
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 _validate_output_options(self, args):
|
|
||||||
unknown_output_options = set(args.output_options) - set(OUTPUT_OPTIONS)
|
|
||||||
if unknown_output_options:
|
|
||||||
self.error('Unknown output options: %s' % ','.join(unknown_output_options))
|
|
||||||
|
|
||||||
def _validate_auth_options(self, args):
|
|
||||||
if args.auth_type and not args.auth:
|
|
||||||
self.error('--auth-type can only be used with --auth')
|
|
||||||
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
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 KeyValueType(object):
|
|
||||||
"""A type used with `argparse`."""
|
|
||||||
|
|
||||||
key_value_class = KeyValue
|
|
||||||
|
|
||||||
def __init__(self, *separators):
|
|
||||||
self.separators = separators
|
|
||||||
self.escapes = ['\\\\' + sep for sep in separators]
|
|
||||||
|
|
||||||
def __call__(self, string):
|
|
||||||
found = {}
|
|
||||||
found_escapes = []
|
|
||||||
for esc in self.escapes:
|
|
||||||
found_escapes += [m.span() for m in re.finditer(esc, string)]
|
|
||||||
for sep in self.separators:
|
|
||||||
matches = re.finditer(sep, string)
|
|
||||||
for match in matches:
|
|
||||||
start, end = match.span()
|
|
||||||
inside_escape = False
|
|
||||||
for estart, eend in found_escapes:
|
|
||||||
if start >= estart and end <= eend:
|
|
||||||
inside_escape = True
|
|
||||||
break
|
|
||||||
if not inside_escape:
|
|
||||||
found[start] = sep
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
raise argparse.ArgumentTypeError(
|
|
||||||
'"%s" is not a valid value' % string)
|
|
||||||
|
|
||||||
# split the string at the earliest non-escaped separator.
|
|
||||||
seploc = min(found.keys())
|
|
||||||
sep = found[seploc]
|
|
||||||
key = string[:seploc]
|
|
||||||
value = string[seploc + len(sep):]
|
|
||||||
|
|
||||||
# remove escape chars
|
|
||||||
for sepstr in self.separators:
|
|
||||||
key = key.replace('\\' + sepstr, sepstr)
|
|
||||||
value = value.replace('\\' + sepstr, sepstr)
|
|
||||||
return 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 AuthCredentialsType(KeyValueType):
|
|
||||||
|
|
||||||
key_value_class = AuthCredentials
|
|
||||||
|
|
||||||
def __call__(self, string):
|
|
||||||
try:
|
|
||||||
return super(AuthCredentialsType, 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_COMMON,
|
|
||||||
orig=string
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_items(items, data=None, headers=None, files=None):
|
|
||||||
"""Parse `KeyValueType` `items` into `data`, `headers` and `files`."""
|
|
||||||
if headers is None:
|
|
||||||
headers = {}
|
|
||||||
if data is None:
|
|
||||||
data = {}
|
|
||||||
if files is None:
|
|
||||||
files = {}
|
|
||||||
for item in items:
|
|
||||||
value = item.value
|
|
||||||
key = item.key
|
|
||||||
if item.sep == SEP_HEADERS:
|
|
||||||
target = headers
|
|
||||||
elif item.sep == SEP_FILES:
|
|
||||||
try:
|
|
||||||
value = open(os.path.expanduser(item.value), 'r')
|
|
||||||
except IOError as e:
|
|
||||||
raise ParseError(
|
|
||||||
'Invalid argument %r. %s' % (item.orig, e))
|
|
||||||
if not key:
|
|
||||||
key = os.path.basename(value.name)
|
|
||||||
target = files
|
|
||||||
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:
|
|
||||||
if item.sep == SEP_DATA_RAW_JSON:
|
|
||||||
try:
|
|
||||||
value = json.loads(item.value)
|
|
||||||
except ValueError:
|
|
||||||
raise ParseError('%s is not valid JSON' % item.orig)
|
|
||||||
target = data
|
|
||||||
else:
|
|
||||||
raise ParseError('%s is not valid item' % item.orig)
|
|
||||||
|
|
||||||
if key in target:
|
|
||||||
ParseError('duplicate item %s (%s)' % (item.key, item.orig))
|
|
||||||
|
|
||||||
target[key] = value
|
|
||||||
|
|
||||||
return headers, data, files
|
|
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
|
@ -1,79 +0,0 @@
|
|||||||
from requests.compat import urlparse
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMessage(object):
|
|
||||||
"""Model representing an HTTP message."""
|
|
||||||
|
|
||||||
def __init__(self, line, headers, body, content_type=None):
|
|
||||||
# {Request,Status}-Line
|
|
||||||
self.line = line
|
|
||||||
self.headers = headers
|
|
||||||
self.body = body
|
|
||||||
self.content_type = content_type
|
|
||||||
|
|
||||||
|
|
||||||
def from_request(request):
|
|
||||||
"""Make an `HTTPMessage` from `requests.models.Request`."""
|
|
||||||
url = urlparse(request.url)
|
|
||||||
request_headers = dict(request.headers)
|
|
||||||
if 'Host' not in request_headers:
|
|
||||||
request_headers['Host'] = url.netloc
|
|
||||||
|
|
||||||
try:
|
|
||||||
body = request.data
|
|
||||||
except AttributeError:
|
|
||||||
# requests < 0.12.1
|
|
||||||
body = request._enc_data
|
|
||||||
|
|
||||||
if isinstance(body, dict):
|
|
||||||
# --form
|
|
||||||
body = request.__class__._encode_params(body)
|
|
||||||
|
|
||||||
return HTTPMessage(
|
|
||||||
line='{method} {path}{query} HTTP/1.1'.format(
|
|
||||||
method=request.method,
|
|
||||||
path=url.path or '/',
|
|
||||||
query='' if url.query is '' else '?' + url.query),
|
|
||||||
headers='\n'.join(str('%s: %s') % (name, value)
|
|
||||||
for name, value
|
|
||||||
in request_headers.items()),
|
|
||||||
body=body,
|
|
||||||
content_type=request_headers.get('Content-Type')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response):
|
|
||||||
"""Make an `HTTPMessage` from `requests.models.Response`."""
|
|
||||||
encoding = response.encoding or 'ISO-8859-1'
|
|
||||||
original = response.raw._original_response
|
|
||||||
response_headers = response.headers
|
|
||||||
return HTTPMessage(
|
|
||||||
line='HTTP/{version} {status} {reason}'.format(
|
|
||||||
version='.'.join(str(original.version)),
|
|
||||||
status=original.status, reason=original.reason),
|
|
||||||
headers=str(original.msg),
|
|
||||||
body=response.content.decode(encoding) if response.content else '',
|
|
||||||
content_type=response_headers.get('Content-Type'))
|
|
||||||
|
|
||||||
|
|
||||||
def format(message, prettifier=None,
|
|
||||||
with_headers=True, with_body=True):
|
|
||||||
"""Return a `unicode` representation of `message`. """
|
|
||||||
pretty = prettifier is not None
|
|
||||||
bits = []
|
|
||||||
|
|
||||||
if with_headers:
|
|
||||||
bits.append(message.line)
|
|
||||||
bits.append(message.headers)
|
|
||||||
if pretty:
|
|
||||||
bits = [prettifier.headers('\n'.join(bits))]
|
|
||||||
if with_body and message.body:
|
|
||||||
bits.append('\n')
|
|
||||||
|
|
||||||
if with_body and message.body:
|
|
||||||
if pretty and message.content_type:
|
|
||||||
bits.append(prettifier.body(message.body, message.content_type))
|
|
||||||
else:
|
|
||||||
bits.append(message.body)
|
|
||||||
|
|
||||||
return '\n'.join(bit.strip() for bit in bits)
|
|
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,59 +0,0 @@
|
|||||||
import os
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pygments
|
|
||||||
|
|
||||||
from pygments.util import ClassNotFound
|
|
||||||
from pygments.styles import get_style_by_name, STYLE_MAP
|
|
||||||
from pygments.lexers import get_lexer_for_mimetype, HttpLexer
|
|
||||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
|
||||||
from pygments.formatters.terminal import TerminalFormatter
|
|
||||||
|
|
||||||
from . import solarized
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_STYLE = 'solarized'
|
|
||||||
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys())
|
|
||||||
FORMATTER = (Terminal256Formatter
|
|
||||||
if '256color' in os.environ.get('TERM', '')
|
|
||||||
else TerminalFormatter)
|
|
||||||
|
|
||||||
application_content_type_re = re.compile(r'application/(.+\+)(json|xml)$')
|
|
||||||
|
|
||||||
|
|
||||||
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]
|
|
||||||
application_match = re.match(application_content_type_re, content_type)
|
|
||||||
if application_match:
|
|
||||||
# Strip vendor and extensions from Content-Type
|
|
||||||
vendor, extension = application_match.groups()
|
|
||||||
content_type = content_type.replace(vendor, '')
|
|
||||||
|
|
||||||
try:
|
|
||||||
lexer = get_lexer_for_mimetype(content_type)
|
|
||||||
except ClassNotFound:
|
|
||||||
return content
|
|
||||||
|
|
||||||
if content_type == "application/json":
|
|
||||||
try:
|
|
||||||
# Indent and sort the JSON data.
|
|
||||||
content = json.dumps(json.loads(content),
|
|
||||||
sort_keys=True, indent=4,
|
|
||||||
ensure_ascii=False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return pygments.highlight(content, lexer, self.formatter)
|
|
4
setup.py
4
setup.py
@ -17,6 +17,9 @@ requirements = [
|
|||||||
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
||||||
# argparse has been added in Python 3.2 / 2.7
|
# argparse has been added in Python 3.2 / 2.7
|
||||||
requirements.append('argparse>=1.2.1')
|
requirements.append('argparse>=1.2.1')
|
||||||
|
if 'win32' in str(sys.platform).lower():
|
||||||
|
# Terminal colors for Windows
|
||||||
|
requirements.append('colorama>=0.2.4')
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@ -52,5 +55,6 @@ setup(
|
|||||||
'Topic :: System :: Networking',
|
'Topic :: System :: Networking',
|
||||||
'Topic :: Terminals',
|
'Topic :: Terminals',
|
||||||
'Topic :: Text Processing',
|
'Topic :: Text Processing',
|
||||||
|
'Topic :: Utilities'
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
593
tests/tests.py
Normal file → Executable file
593
tests/tests.py
Normal file → Executable file
@ -1,12 +1,32 @@
|
|||||||
# -*- coding: utf-8 -*-
|
#!/usr/bin/env python
|
||||||
import unittest
|
# coding=utf8
|
||||||
import argparse
|
"""
|
||||||
|
|
||||||
|
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 os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import json
|
import json
|
||||||
from requests.compat import is_py26, is_py3
|
import tempfile
|
||||||
from requests import Response
|
import unittest
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
from requests.compat import is_py26, is_py3, str
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
@ -17,32 +37,83 @@ from requests import Response
|
|||||||
TESTS_ROOT = os.path.dirname(__file__)
|
TESTS_ROOT = os.path.dirname(__file__)
|
||||||
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
||||||
|
|
||||||
from httpie import __main__, cliparse
|
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_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
|
||||||
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
||||||
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
||||||
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
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):
|
def http(*args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Invoke `httpie.__main__.main` with `args` and `kwargs`,
|
Invoke `httpie.core.main()` with `args` and `kwargs`,
|
||||||
and return a unicode response.
|
and return a unicode response.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
http_kwargs = {
|
|
||||||
'stdin_isatty': True,
|
if 'env' not in kwargs:
|
||||||
'stdout_isatty': False
|
# Ensure that we have terminal by default (needed for Travis).
|
||||||
}
|
kwargs['env'] = Environment(
|
||||||
http_kwargs.update(kwargs)
|
colors=0,
|
||||||
stdout = http_kwargs.setdefault('stdout', tempfile.TemporaryFile())
|
stdin_isatty=True,
|
||||||
__main__.main(args=args, **http_kwargs)
|
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)
|
stdout.seek(0)
|
||||||
response = stdout.read().decode('utf8')
|
stderr.seek(0)
|
||||||
|
|
||||||
|
r = Response(stdout.read().decode('utf8'))
|
||||||
|
r.stderr = stderr.read().decode('utf8')
|
||||||
|
r.exit_status = exit_status
|
||||||
|
|
||||||
stdout.close()
|
stdout.close()
|
||||||
return response
|
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):
|
class BaseTestCase(unittest.TestCase):
|
||||||
@ -60,47 +131,145 @@ class BaseTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
# High-level tests using httpbin.org.
|
# High-level tests using httpbin.
|
||||||
#################################################################
|
#################################################################
|
||||||
|
|
||||||
class HTTPieTest(BaseTestCase):
|
class HTTPieTest(BaseTestCase):
|
||||||
|
|
||||||
def test_GET(self):
|
def test_GET(self):
|
||||||
r = http('GET', 'http://httpbin.org/get')
|
r = http(
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
|
||||||
def test_DELETE(self):
|
def test_DELETE(self):
|
||||||
r = http('DELETE', 'http://httpbin.org/delete')
|
r = http(
|
||||||
|
'DELETE',
|
||||||
|
httpbin('/delete')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
|
||||||
def test_PUT(self):
|
def test_PUT(self):
|
||||||
r = http('PUT', 'http://httpbin.org/put', 'foo=bar')
|
r = http(
|
||||||
|
'PUT',
|
||||||
|
httpbin('/put'),
|
||||||
|
'foo=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"foo": "bar"', r)
|
self.assertIn('"foo": "bar"', r)
|
||||||
|
|
||||||
def test_POST_JSON_data(self):
|
def test_POST_JSON_data(self):
|
||||||
r = http('POST', 'http://httpbin.org/post', 'foo=bar')
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'foo=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"foo": "bar"', r)
|
self.assertIn('"foo": "bar"', r)
|
||||||
|
|
||||||
def test_POST_form(self):
|
def test_POST_form(self):
|
||||||
r = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
|
r = http(
|
||||||
|
'--form',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'foo=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"foo": "bar"', 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):
|
def test_POST_stdin(self):
|
||||||
r = http('--form', 'POST', 'http://httpbin.org/post',
|
|
||||||
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
|
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('HTTP/1.1 200', r)
|
||||||
self.assertIn(TEST_FILE_CONTENT, r)
|
self.assertIn(TEST_FILE_CONTENT, r)
|
||||||
|
|
||||||
def test_headers(self):
|
def test_headers(self):
|
||||||
r = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
|
r = http(
|
||||||
|
'GET',
|
||||||
|
httpbin('/headers'),
|
||||||
|
'Foo:bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"User-Agent": "HTTPie', r)
|
self.assertIn('"User-Agent": "HTTPie', r)
|
||||||
self.assertIn('"Foo": "bar"', 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):
|
class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
Test that Accept and Content-Type correctly defaults to JSON,
|
Test that Accept and Content-Type correctly defaults to JSON,
|
||||||
@ -110,84 +279,159 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
|
|||||||
"""
|
"""
|
||||||
def test_GET_no_data_no_auto_headers(self):
|
def test_GET_no_data_no_auto_headers(self):
|
||||||
# https://github.com/jkbr/httpie/issues/62
|
# https://github.com/jkbr/httpie/issues/62
|
||||||
r = http('GET', 'http://httpbin.org/headers')
|
r = http(
|
||||||
|
'GET',
|
||||||
|
httpbin('/headers')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "*/*"', r)
|
self.assertIn('"Accept": "*/*"', r)
|
||||||
# Although an empty header is present in the response from httpbin,
|
self.assertNotIn('"Content-Type": "application/json', r)
|
||||||
# it's not included in the request.
|
|
||||||
self.assertIn('"Content-Type": ""', r)
|
|
||||||
|
|
||||||
def test_POST_no_data_no_auto_headers(self):
|
def test_POST_no_data_no_auto_headers(self):
|
||||||
# JSON headers shouldn't be automatically set for POST with no data.
|
# JSON headers shouldn't be automatically set for POST with no data.
|
||||||
r = http('POST', 'http://httpbin.org/post')
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "*/*"', r)
|
self.assertIn('"Accept": "*/*"', r)
|
||||||
# Although an empty header is present in the response from httpbin,
|
self.assertNotIn('"Content-Type": "application/json', r)
|
||||||
# it's not included in the request.
|
|
||||||
self.assertIn(' "Content-Type": ""', r)
|
|
||||||
|
|
||||||
def test_POST_with_data_auto_JSON_headers(self):
|
def test_POST_with_data_auto_JSON_headers(self):
|
||||||
r = http('POST', 'http://httpbin.org/post', 'a=b')
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'a=b'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "application/json"', r)
|
self.assertIn('"Accept": "application/json"', r)
|
||||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||||
|
|
||||||
def test_GET_with_data_auto_JSON_headers(self):
|
def test_GET_with_data_auto_JSON_headers(self):
|
||||||
# JSON headers should automatically be set also for GET with data.
|
# JSON headers should automatically be set also for GET with data.
|
||||||
r = http('POST', 'http://httpbin.org/post', 'a=b')
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'a=b'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "application/json"', r)
|
self.assertIn('"Accept": "application/json"', r)
|
||||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||||
|
|
||||||
def test_POST_explicit_JSON_auto_JSON_headers(self):
|
def test_POST_explicit_JSON_auto_JSON_headers(self):
|
||||||
r = http('-j', 'POST', 'http://httpbin.org/post')
|
r = http(
|
||||||
|
'--json',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "application/json"', r)
|
self.assertIn('"Accept": "application/json"', r)
|
||||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||||
|
|
||||||
def test_GET_explicit_JSON_explicit_headers(self):
|
def test_GET_explicit_JSON_explicit_headers(self):
|
||||||
r = http('-j', 'GET', 'http://httpbin.org/headers',
|
r = http(
|
||||||
'Accept:application/xml',
|
'--json',
|
||||||
'Content-Type:application/xml')
|
'GET',
|
||||||
|
httpbin('/headers'),
|
||||||
|
'Accept:application/xml',
|
||||||
|
'Content-Type:application/xml'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Accept": "application/xml"', r)
|
self.assertIn('"Accept": "application/xml"', r)
|
||||||
self.assertIn('"Content-Type": "application/xml"', r)
|
self.assertIn('"Content-Type": "application/xml"', r)
|
||||||
|
|
||||||
def test_POST_form_auto_Content_Type(self):
|
def test_POST_form_auto_Content_Type(self):
|
||||||
r = http('-f', 'POST', 'http://httpbin.org/post')
|
r = http(
|
||||||
|
'--form',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"', r)
|
self.assertIn(
|
||||||
|
'"Content-Type":'
|
||||||
|
' "application/x-www-form-urlencoded; charset=utf-8"',
|
||||||
|
r
|
||||||
|
)
|
||||||
|
|
||||||
def test_POST_form_Content_Type_override(self):
|
def test_POST_form_Content_Type_override(self):
|
||||||
r = http('-f', 'POST', 'http://httpbin.org/post', 'Content-Type:application/xml')
|
r = http(
|
||||||
|
'--form',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'Content-Type:application/xml'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Content-Type": "application/xml"', 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):
|
class ImplicitHTTPMethodTest(BaseTestCase):
|
||||||
|
|
||||||
def test_implicit_GET(self):
|
def test_implicit_GET(self):
|
||||||
r = http('http://httpbin.org/get')
|
r = http(httpbin('/get'))
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
|
||||||
def test_implicit_GET_with_headers(self):
|
def test_implicit_GET_with_headers(self):
|
||||||
r = http('http://httpbin.org/headers', 'Foo:bar')
|
r = http(
|
||||||
|
httpbin('/headers'),
|
||||||
|
'Foo:bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"Foo": "bar"', r)
|
self.assertIn('"Foo": "bar"', r)
|
||||||
|
|
||||||
def test_implicit_POST_json(self):
|
def test_implicit_POST_json(self):
|
||||||
r = http('http://httpbin.org/post', 'hello=world')
|
r = http(
|
||||||
|
httpbin('/post'),
|
||||||
|
'hello=world'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"hello": "world"', r)
|
self.assertIn('"hello": "world"', r)
|
||||||
|
|
||||||
def test_implicit_POST_form(self):
|
def test_implicit_POST_form(self):
|
||||||
r = http('--form', 'http://httpbin.org/post', 'foo=bar')
|
r = http(
|
||||||
|
'--form',
|
||||||
|
httpbin('/post'),
|
||||||
|
'foo=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"foo": "bar"', r)
|
self.assertIn('"foo": "bar"', r)
|
||||||
|
|
||||||
def test_implicit_POST_stdin(self):
|
def test_implicit_POST_stdin(self):
|
||||||
r = http('--form', 'http://httpbin.org/post',
|
env = Environment(
|
||||||
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
|
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)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
|
|
||||||
|
|
||||||
@ -195,32 +439,66 @@ class PrettyFlagTest(BaseTestCase):
|
|||||||
"""Test the --pretty / --ugly flag handling."""
|
"""Test the --pretty / --ugly flag handling."""
|
||||||
|
|
||||||
def test_pretty_enabled_by_default(self):
|
def test_pretty_enabled_by_default(self):
|
||||||
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
|
r = http(
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=Environment(
|
||||||
|
stdin_isatty=True,
|
||||||
|
stdout_isatty=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
||||||
|
|
||||||
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
|
def test_pretty_enabled_by_default_unless_stdout_redirected(self):
|
||||||
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
|
r = http(
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
||||||
|
|
||||||
def test_force_pretty(self):
|
def test_force_pretty(self):
|
||||||
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
|
r = http(
|
||||||
|
'--pretty',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=Environment(
|
||||||
|
stdin_isatty=True,
|
||||||
|
stdout_isatty=False
|
||||||
|
),
|
||||||
|
)
|
||||||
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
||||||
|
|
||||||
def test_force_ugly(self):
|
def test_force_ugly(self):
|
||||||
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
|
r = http(
|
||||||
|
'--ugly',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
)
|
||||||
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
||||||
|
|
||||||
|
|
||||||
class VerboseFlagTest(BaseTestCase):
|
class VerboseFlagTest(BaseTestCase):
|
||||||
|
|
||||||
def test_verbose(self):
|
def test_verbose(self):
|
||||||
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
|
r = http(
|
||||||
|
'--verbose',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
'test-header:__test__'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertEqual(r.count('__test__'), 2)
|
self.assertEqual(r.count('__test__'), 2)
|
||||||
|
|
||||||
def test_verbose_form(self):
|
def test_verbose_form(self):
|
||||||
# https://github.com/jkbr/httpie/issues/53
|
# https://github.com/jkbr/httpie/issues/53
|
||||||
r = http('--verbose', '--form', 'POST', 'http://httpbin.org/post', 'foo=bar', 'baz=bar')
|
r = http(
|
||||||
|
'--verbose',
|
||||||
|
'--form',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'foo=bar',
|
||||||
|
'baz=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('foo=bar&baz=bar', r)
|
self.assertIn('foo=bar&baz=bar', r)
|
||||||
|
|
||||||
@ -228,14 +506,22 @@ class VerboseFlagTest(BaseTestCase):
|
|||||||
class MultipartFormDataFileUploadTest(BaseTestCase):
|
class MultipartFormDataFileUploadTest(BaseTestCase):
|
||||||
|
|
||||||
def test_non_existent_file_raises_parse_error(self):
|
def test_non_existent_file_raises_parse_error(self):
|
||||||
self.assertRaises(cliparse.ParseError, http,
|
self.assertRaises(input.ParseError, http,
|
||||||
'--form', '--traceback',
|
'--form',
|
||||||
'POST', 'http://httpbin.org/post',
|
'--traceback',
|
||||||
'foo@/__does_not_exist__')
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'foo@/__does_not_exist__'
|
||||||
|
)
|
||||||
|
|
||||||
def test_upload_ok(self):
|
def test_upload_ok(self):
|
||||||
r = http('--form', 'POST', 'http://httpbin.org/post',
|
r = http(
|
||||||
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
|
'--form',
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'test-file@%s' % TEST_FILE_PATH,
|
||||||
|
'foo=bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
|
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
|
||||||
self.assertIn('"foo": "bar"', r)
|
self.assertIn('"foo": "bar"', r)
|
||||||
@ -247,13 +533,22 @@ class RequestBodyFromFilePathTest(BaseTestCase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
def test_request_body_from_file_by_path(self):
|
def test_request_body_from_file_by_path(self):
|
||||||
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH)
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'@' + TEST_FILE_PATH
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn(TEST_FILE_CONTENT, r)
|
self.assertIn(TEST_FILE_CONTENT, r)
|
||||||
self.assertIn('"Content-Type": "text/plain"', r)
|
self.assertIn('"Content-Type": "text/plain"', r)
|
||||||
|
|
||||||
def test_request_body_from_file_by_path_with_explicit_content_type(self):
|
def test_request_body_from_file_by_path_with_explicit_content_type(self):
|
||||||
r = http('POST', 'http://httpbin.org/post', '@' + TEST_FILE_PATH, 'Content-Type:x-foo/bar')
|
r = http(
|
||||||
|
'POST',
|
||||||
|
httpbin('/post'),
|
||||||
|
'@' + TEST_FILE_PATH,
|
||||||
|
'Content-Type:x-foo/bar'
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn(TEST_FILE_CONTENT, r)
|
self.assertIn(TEST_FILE_CONTENT, r)
|
||||||
self.assertIn('"Content-Type": "x-foo/bar"', r)
|
self.assertIn('"Content-Type": "x-foo/bar"', r)
|
||||||
@ -261,45 +556,124 @@ class RequestBodyFromFilePathTest(BaseTestCase):
|
|||||||
def test_request_body_from_file_by_path_only_one_file_allowed(self):
|
def test_request_body_from_file_by_path_only_one_file_allowed(self):
|
||||||
self.assertRaises(SystemExit, lambda: http(
|
self.assertRaises(SystemExit, lambda: http(
|
||||||
'POST',
|
'POST',
|
||||||
'http://httpbin.org/post',
|
httpbin('/post'),
|
||||||
'@' + TEST_FILE_PATH,
|
'@' + TEST_FILE_PATH,
|
||||||
'@' + TEST_FILE2_PATH))
|
'@' + TEST_FILE2_PATH)
|
||||||
|
)
|
||||||
|
|
||||||
def test_request_body_from_file_by_path_no_data_items_allowed(self):
|
def test_request_body_from_file_by_path_no_data_items_allowed(self):
|
||||||
self.assertRaises(SystemExit, lambda: http(
|
self.assertRaises(SystemExit, lambda: http(
|
||||||
'POST',
|
'POST',
|
||||||
'http://httpbin.org/post',
|
httpbin('/post'),
|
||||||
'@' + TEST_FILE_PATH,
|
'@' + TEST_FILE_PATH,
|
||||||
'foo=bar'))
|
'foo=bar')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthTest(BaseTestCase):
|
class AuthTest(BaseTestCase):
|
||||||
|
|
||||||
def test_basic_auth(self):
|
def test_basic_auth(self):
|
||||||
r = http('--auth', 'user:password',
|
r = http(
|
||||||
'GET', 'httpbin.org/basic-auth/user/password')
|
'--auth=user:password',
|
||||||
|
'GET',
|
||||||
|
httpbin('/basic-auth/user/password')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"authenticated": true', r)
|
self.assertIn('"authenticated": true', r)
|
||||||
self.assertIn('"user": "user"', r)
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
def test_digest_auth(self):
|
def test_digest_auth(self):
|
||||||
r = http('--auth-type=digest', '--auth', 'user:password',
|
r = http(
|
||||||
'GET', 'httpbin.org/digest-auth/auth/user/password')
|
'--auth-type=digest',
|
||||||
|
'--auth=user:password',
|
||||||
|
'GET',
|
||||||
|
httpbin('/digest-auth/auth/user/password')
|
||||||
|
)
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"authenticated": true', r)
|
self.assertIn('"authenticated": true', r)
|
||||||
self.assertIn('"user": "user"', r)
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
def test_password_prompt(self):
|
def test_password_prompt(self):
|
||||||
cliparse.AuthCredentials._getpass = lambda self, prompt: 'password'
|
|
||||||
|
|
||||||
r = http('--auth', 'user',
|
input.AuthCredentials._getpass = lambda self, prompt: 'password'
|
||||||
'GET', 'httpbin.org/basic-auth/user/password')
|
|
||||||
|
r = http(
|
||||||
|
'--auth',
|
||||||
|
'user',
|
||||||
|
'GET',
|
||||||
|
httpbin('/basic-auth/user/password')
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIn('HTTP/1.1 200', r)
|
self.assertIn('HTTP/1.1 200', r)
|
||||||
self.assertIn('"authenticated": true', r)
|
self.assertIn('"authenticated": true', r)
|
||||||
self.assertIn('"user": "user"', 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.
|
# CLI argument parsing related tests.
|
||||||
#################################################################
|
#################################################################
|
||||||
@ -307,11 +681,12 @@ class AuthTest(BaseTestCase):
|
|||||||
class ItemParsingTest(BaseTestCase):
|
class ItemParsingTest(BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.key_value_type = cliparse.KeyValueType(
|
self.key_value_type = input.KeyValueArgType(
|
||||||
cliparse.SEP_HEADERS,
|
input.SEP_HEADERS,
|
||||||
cliparse.SEP_DATA,
|
input.SEP_QUERY,
|
||||||
cliparse.SEP_DATA_RAW_JSON,
|
input.SEP_DATA,
|
||||||
cliparse.SEP_FILES,
|
input.SEP_DATA_RAW_JSON,
|
||||||
|
input.SEP_FILES,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_items(self):
|
def test_invalid_items(self):
|
||||||
@ -321,7 +696,7 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
lambda: self.key_value_type(item))
|
lambda: self.key_value_type(item))
|
||||||
|
|
||||||
def test_escape(self):
|
def test_escape(self):
|
||||||
headers, data, files = cliparse.parse_items([
|
headers, data, files, params = input.parse_items([
|
||||||
# headers
|
# headers
|
||||||
self.key_value_type('foo\\:bar:baz'),
|
self.key_value_type('foo\\:bar:baz'),
|
||||||
self.key_value_type('jack\\@jill:hill'),
|
self.key_value_type('jack\\@jill:hill'),
|
||||||
@ -340,15 +715,15 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
self.assertIn('bar@baz', files)
|
self.assertIn('bar@baz', files)
|
||||||
|
|
||||||
def test_escape_longsep(self):
|
def test_escape_longsep(self):
|
||||||
headers, data, files = cliparse.parse_items([
|
headers, data, files, params = input.parse_items([
|
||||||
self.key_value_type('bob\\:==foo'),
|
self.key_value_type('bob\\:==foo'),
|
||||||
])
|
])
|
||||||
self.assertDictEqual(data, {
|
self.assertDictEqual(params, {
|
||||||
'bob:=': 'foo',
|
'bob:': 'foo',
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_valid_items(self):
|
def test_valid_items(self):
|
||||||
headers, data, files = cliparse.parse_items([
|
headers, data, files, params = input.parse_items([
|
||||||
self.key_value_type('string=value'),
|
self.key_value_type('string=value'),
|
||||||
self.key_value_type('header:value'),
|
self.key_value_type('header:value'),
|
||||||
self.key_value_type('list:=["a", 1, {}, false]'),
|
self.key_value_type('list:=["a", 1, {}, false]'),
|
||||||
@ -357,6 +732,7 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
self.key_value_type('ed='),
|
self.key_value_type('ed='),
|
||||||
self.key_value_type('bool:=true'),
|
self.key_value_type('bool:=true'),
|
||||||
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
|
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
|
||||||
|
self.key_value_type('query==value'),
|
||||||
])
|
])
|
||||||
self.assertDictEqual(headers, {
|
self.assertDictEqual(headers, {
|
||||||
'header': 'value',
|
'header': 'value',
|
||||||
@ -367,7 +743,10 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
"string": "value",
|
"string": "value",
|
||||||
"bool": True,
|
"bool": True,
|
||||||
"list": ["a", 1, {}, False],
|
"list": ["a", 1, {}, False],
|
||||||
"obj": {"a": "b"}
|
"obj": {"a": "b"},
|
||||||
|
})
|
||||||
|
self.assertDictEqual(params, {
|
||||||
|
'query': 'value',
|
||||||
})
|
})
|
||||||
self.assertIn('test-file', files)
|
self.assertIn('test-file', files)
|
||||||
|
|
||||||
@ -375,7 +754,7 @@ class ItemParsingTest(BaseTestCase):
|
|||||||
class ArgumentParserTestCase(unittest.TestCase):
|
class ArgumentParserTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.parser = cliparse.Parser()
|
self.parser = input.Parser()
|
||||||
|
|
||||||
def test_guess_when_method_set_and_valid(self):
|
def test_guess_when_method_set_and_valid(self):
|
||||||
args = argparse.Namespace()
|
args = argparse.Namespace()
|
||||||
@ -383,7 +762,7 @@ class ArgumentParserTestCase(unittest.TestCase):
|
|||||||
args.url = 'http://example.com/'
|
args.url = 'http://example.com/'
|
||||||
args.items = []
|
args.items = []
|
||||||
|
|
||||||
self.parser._guess_method(args)
|
self.parser._guess_method(args, Environment())
|
||||||
|
|
||||||
self.assertEquals(args.method, 'GET')
|
self.assertEquals(args.method, 'GET')
|
||||||
self.assertEquals(args.url, 'http://example.com/')
|
self.assertEquals(args.url, 'http://example.com/')
|
||||||
@ -395,7 +774,10 @@ class ArgumentParserTestCase(unittest.TestCase):
|
|||||||
args.url = 'http://example.com/'
|
args.url = 'http://example.com/'
|
||||||
args.items = []
|
args.items = []
|
||||||
|
|
||||||
self.parser._guess_method(args)
|
self.parser._guess_method(args, Environment(
|
||||||
|
stdin_isatty=True,
|
||||||
|
stdout_isatty=True,
|
||||||
|
))
|
||||||
|
|
||||||
self.assertEquals(args.method, 'GET')
|
self.assertEquals(args.method, 'GET')
|
||||||
self.assertEquals(args.url, 'http://example.com/')
|
self.assertEquals(args.url, 'http://example.com/')
|
||||||
@ -407,13 +789,14 @@ class ArgumentParserTestCase(unittest.TestCase):
|
|||||||
args.url = 'data=field'
|
args.url = 'data=field'
|
||||||
args.items = []
|
args.items = []
|
||||||
|
|
||||||
self.parser._guess_method(args)
|
self.parser._guess_method(args, Environment())
|
||||||
|
|
||||||
self.assertEquals(args.method, 'POST')
|
self.assertEquals(args.method, 'POST')
|
||||||
self.assertEquals(args.url, 'http://example.com/')
|
self.assertEquals(args.url, 'http://example.com/')
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
args.items,
|
args.items,
|
||||||
[cliparse.KeyValue(key='data', value='field', sep='=', orig='data=field')])
|
[input.KeyValue(
|
||||||
|
key='data', value='field', sep='=', orig='data=field')])
|
||||||
|
|
||||||
def test_guess_when_method_set_but_invalid_and_header_field(self):
|
def test_guess_when_method_set_but_invalid_and_header_field(self):
|
||||||
args = argparse.Namespace()
|
args = argparse.Namespace()
|
||||||
@ -421,31 +804,38 @@ class ArgumentParserTestCase(unittest.TestCase):
|
|||||||
args.url = 'test:header'
|
args.url = 'test:header'
|
||||||
args.items = []
|
args.items = []
|
||||||
|
|
||||||
self.parser._guess_method(args)
|
self.parser._guess_method(args, Environment(
|
||||||
|
stdin_isatty=True,
|
||||||
|
stdout_isatty=True,
|
||||||
|
))
|
||||||
|
|
||||||
self.assertEquals(args.method, 'GET')
|
self.assertEquals(args.method, 'GET')
|
||||||
self.assertEquals(args.url, 'http://example.com/')
|
self.assertEquals(args.url, 'http://example.com/')
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
args.items,
|
args.items,
|
||||||
[cliparse.KeyValue(key='test', value='header', sep=':', orig='test:header')])
|
[input.KeyValue(
|
||||||
|
key='test', value='header', sep=':', orig='test:header')])
|
||||||
|
|
||||||
def test_guess_when_method_set_but_invalid_and_item_exists(self):
|
def test_guess_when_method_set_but_invalid_and_item_exists(self):
|
||||||
args = argparse.Namespace()
|
args = argparse.Namespace()
|
||||||
args.method = 'http://example.com/'
|
args.method = 'http://example.com/'
|
||||||
args.url = 'new_item=a'
|
args.url = 'new_item=a'
|
||||||
args.items = [
|
args.items = [
|
||||||
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b')
|
input.KeyValue(
|
||||||
|
key='old_item', value='b', sep='=', orig='old_item=b')
|
||||||
]
|
]
|
||||||
|
|
||||||
self.parser._guess_method(args)
|
self.parser._guess_method(args, Environment())
|
||||||
|
|
||||||
self.assertEquals(args.items, [
|
self.assertEquals(args.items, [
|
||||||
cliparse.KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
|
input.KeyValue(
|
||||||
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b'),
|
key='new_item', value='a', sep='=', orig='new_item=a'),
|
||||||
|
input.KeyValue(key
|
||||||
|
='old_item', value='b', sep='=', orig='old_item=b'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class FakeResponse(Response):
|
class FakeResponse(requests.Response):
|
||||||
|
|
||||||
class Mock(object):
|
class Mock(object):
|
||||||
|
|
||||||
@ -489,7 +879,7 @@ class UnicodeOutputTestCase(BaseTestCase):
|
|||||||
args.style = 'default'
|
args.style = 'default'
|
||||||
|
|
||||||
# colorized output contains escape sequences
|
# colorized output contains escape sequences
|
||||||
output = __main__._get_output(args, True, response)
|
output = get_output(args, Environment(), response.request, response)
|
||||||
|
|
||||||
for key, value in response_dict.items():
|
for key, value in response_dict.items():
|
||||||
self.assertIn(key, output)
|
self.assertIn(key, output)
|
||||||
@ -497,4 +887,5 @@ class UnicodeOutputTestCase(BaseTestCase):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
#noinspection PyCallingNonCallable
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
2
tox.ini
2
tox.ini
@ -4,7 +4,7 @@
|
|||||||
# and then run "tox" from this directory.
|
# and then run "tox" from this directory.
|
||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py26, py27, py30, py31, py32, pypy
|
envlist = py26, py27, py32, pypy
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = {envpython} setup.py test
|
commands = {envpython} setup.py test
|
||||||
|
Reference in New Issue
Block a user