mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 10:27:47 +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.7
|
||||
- pypy
|
||||
- 3.1
|
||||
- 3.2
|
||||
script: python setup.py test
|
||||
install:
|
||||
|
296
README.rst
296
README.rst
@ -1,22 +1,33 @@
|
||||
=======================
|
||||
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 does so by providing an ``http`` command that allows for issuing arbitrary HTTP requests using a **simple and natural syntax** and displaying **colorized responses**:
|
||||
**HTTPie is a CLI HTTP utility** built out of frustration with existing tools.
|
||||
Its goal is to make CLI interaction with HTTP-based services as
|
||||
**human-friendly** as possible. HTTPie provides an ``http`` command that allows
|
||||
for issuing **arbitrary HTTP** requests using a **simple and natural syntax**,
|
||||
and displays **colorized responses**:
|
||||
|
||||
.. image:: https://github.com/jkbr/httpie/raw/master/httpie.png
|
||||
:alt: HTTPie compared to cURL
|
||||
|
||||
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
|
||||
------------
|
||||
============
|
||||
|
||||
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
|
||||
|
||||
# easy_install pip
|
||||
|
||||
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
|
||||
|
||||
|
||||
There are packages available for `Ubuntu <http://packages.ubuntu.com/quantal/httpie>`_ and `Debian <http://packages.debian.org/wheezy/httpie>`_.
|
||||
There are also packages available for `Ubuntu`_, `Debian`_ and possibly other
|
||||
distributions as well.
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
=====
|
||||
|
||||
Hello world::
|
||||
|
||||
@ -43,28 +54,53 @@ Synopsis::
|
||||
|
||||
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``)
|
||||
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]``.
|
||||
|
||||
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.
|
||||
All ``items`` come after the URL, and, unlike ``flags``, they become part of
|
||||
the actual request being is sent. Their types are distinguished by the
|
||||
separator used.
|
||||
|
||||
|
||||
Examples
|
||||
^^^^^^^^
|
||||
--------
|
||||
::
|
||||
|
||||
http PATCH api.example.com/person/1 X-API-Token:123 name=John email=john@example.org age:=29
|
||||
|
||||
|
||||
The following request is issued::
|
||||
|
||||
PATCH /person/1 HTTP/1.1
|
||||
@ -74,8 +110,8 @@ The following request is issued::
|
||||
|
||||
{"name": "John", "email": "john@example.org", "age": 29}
|
||||
|
||||
|
||||
It can easily be changed to a **form** request using the ``-f`` (or ``--form``) flag, which produces::
|
||||
It can easily be changed to a **form** request using the ``-f``
|
||||
(or ``--form``) flag, which produces::
|
||||
|
||||
PATCH /person/1 HTTP/1.1
|
||||
User-Agent: HTTPie/0.1
|
||||
@ -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
|
||||
|
||||
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
|
||||
|
||||
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">
|
||||
<input type="text" name="name" />
|
||||
<input type="file" name="cv" />
|
||||
</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
|
||||
# Or:
|
||||
http POST example.com/person/1 X-API-Token:123 < person.json
|
||||
|
||||
That can be used for **piping services together**. The following example ``GET``s JSON data from the Github API and ``POST``s it to httpbin.org::
|
||||
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
|
||||
|
||||
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
|
||||
^^^^^
|
||||
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
|
||||
-----
|
||||
|
||||
$ http --help
|
||||
usage: http [-h] [--version] [--json | --form] [--traceback]
|
||||
``$ http --help``::
|
||||
|
||||
usage: http [--help] [--version] [--json | --form] [--traceback]
|
||||
[--pretty | --ugly]
|
||||
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
|
||||
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
|
||||
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
|
||||
[--timeout TIMEOUT]
|
||||
[--style STYLE] [--check-status] [--auth AUTH]
|
||||
[--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY]
|
||||
[--allow-redirects] [--timeout TIMEOUT]
|
||||
[METHOD] URL [ITEM [ITEM ...]]
|
||||
|
||||
HTTPie - cURL for humans. <http://httpie.org>
|
||||
@ -141,12 +218,13 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
||||
separator used. It can be an HTTP header
|
||||
(header:value), a data field to be used in the request
|
||||
body (field_name=value), a raw JSON data field
|
||||
(field_name:=value), or a file field
|
||||
(field_name@/path/to/file). You can use a backslash to
|
||||
escape a colliding separator in the field name.
|
||||
(field_name:=value), a query parameter (name==value),
|
||||
or a file field (field_name@/path/to/file). You can
|
||||
use a backslash to escape a colliding separator in the
|
||||
field name.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--help show this help message and exit
|
||||
--version show program's version number and exit
|
||||
--json, -j (default) Data items from the command line are
|
||||
serialized as a JSON object. The Content-Type and
|
||||
@ -164,14 +242,17 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
||||
redirected.
|
||||
--ugly, -u Do not prettify the response.
|
||||
--print OUTPUT_OPTIONS, -p OUTPUT_OPTIONS
|
||||
String specifying what should the output contain. "H"
|
||||
stands for the request headers and "B" for the request
|
||||
body. "h" stands for the response headers and "b" for
|
||||
response the body. Defaults to "hb" which means that
|
||||
the whole response (headers and body) is printed.
|
||||
String specifying what the output should contain: "H"
|
||||
stands for the request headers, and "B" for the
|
||||
request body. "h" stands for the response headers and
|
||||
"b" for response the body. The default behaviour is
|
||||
"hb" (i.e., the response headers and body is printed),
|
||||
if standard output is not redirected. If the output is
|
||||
piped to another program or to a file, then only the
|
||||
body is printed by default.
|
||||
--verbose, -v Print the whole request as well as the response.
|
||||
Shortcut for --print=HBhb.
|
||||
--headers, -t Print only the response headers. Shortcut for
|
||||
--headers, -h Print only the response headers. Shortcut for
|
||||
--print=h.
|
||||
--body, -b Print only the response body. Shortcut for --print=b.
|
||||
--style STYLE, -s STYLE
|
||||
@ -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
|
||||
to "xterm-256color" or similar (e.g., via `export TERM
|
||||
=xterm-256color' in your ~/.bashrc).
|
||||
--auth AUTH, -a AUTH username:password. If the password is omitted (-a
|
||||
username), HTTPie will prompt for it.
|
||||
--check-status By default, HTTPie exits with 0 when no network or
|
||||
other fatal errors occur. This flag instructs HTTPie
|
||||
to also check the HTTP status code and exit with an
|
||||
error if the status indicates one. When the server
|
||||
replies with a 4xx (Client Error) or 5xx (Server
|
||||
Error) status code, HTTPie exits with 4 or 5
|
||||
respectively. If the response is a 3xx (Redirect) and
|
||||
--allow-redirects hasn't been set, then the exit
|
||||
status is 3. Also an error message is written to
|
||||
stderr if stdout is redirected.
|
||||
--auth AUTH, -a AUTH username:password. If only the username is provided
|
||||
(-a username), HTTPie will prompt for the password.
|
||||
--auth-type {basic,digest}
|
||||
The authentication mechanism to be used. Defaults to
|
||||
"basic".
|
||||
@ -202,19 +293,28 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
|
||||
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
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
|
||||
tox
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
* `0.2.5 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.5>`_ (2012-07-17)
|
||||
* Unicode characters in prettified JSON now don't get escaped to improve readability.
|
||||
Changelog
|
||||
=========
|
||||
|
||||
* `0.2.7dev`_
|
||||
* `0.2.6`_ (2012-07-26)
|
||||
* The short option for ``--headers`` is now ``-h`` (``-t`` has been
|
||||
removed, for usage use ``--help``).
|
||||
* Form data and URL parameters can have multiple fields with the same name
|
||||
(e.g.,``http -f url a=1 a=2``).
|
||||
* Added ``--check-status`` to exit with an error on HTTP 3xx, 4xx and
|
||||
5xx (3, 4, and 5, respectively).
|
||||
* If the output is piped to another program or redirected to a file,
|
||||
the default behaviour is to only print the response body.
|
||||
(It can still be overwritten via the ``--print`` flag.)
|
||||
* Improved highlighting of HTTP headers.
|
||||
* Added query string parameters (``param==value``).
|
||||
* Added support for terminal colors under Windows.
|
||||
* `0.2.5`_ (2012-07-17)
|
||||
* Unicode characters in prettified JSON now don't get escaped for
|
||||
improved readability.
|
||||
* --auth now prompts for a password if only a username provided.
|
||||
* Added support for request payloads from a file path with automatic ``Content-Type`` (``http URL @/path``).
|
||||
* Fixed missing query string when displaing the request headers via ``--verbose``.
|
||||
* Added support for request payloads from a file path with automatic
|
||||
``Content-Type`` (``http URL @/path``).
|
||||
* Fixed missing query string when displaying the request headers via
|
||||
``--verbose``.
|
||||
* Fixed Content-Type for requests with no data.
|
||||
* `0.2.2 <https://github.com/jkbr/httpie/compare/0.2.1...0.2.2>`_ (2012-06-24)
|
||||
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
|
||||
* `0.2.2`_ (2012-06-24)
|
||||
* The ``METHOD`` positional argument can now be omitted (defaults to
|
||||
``GET``, or to ``POST`` with data).
|
||||
* Fixed --verbose --form.
|
||||
* Added support for `Tox <http://tox.testrun.org/>`_.
|
||||
* `0.2.1 <https://github.com/jkbr/httpie/compare/0.2.0...0.2.1>`_ (2012-06-13)
|
||||
* Added support for `Tox`_.
|
||||
* `0.2.1`_ (2012-06-13)
|
||||
* Added compatibility with ``requests-0.12.1``.
|
||||
* Dropped custom JSON and HTTP lexers in favor of the ones newly included in ``pygments-1.5``.
|
||||
* `0.2.0 <https://github.com/jkbr/httpie/compare/0.1.6...0.2.0>`_ (2012-04-25)
|
||||
* Dropped custom JSON and HTTP lexers in favor of the ones newly included
|
||||
in ``pygments-1.5``.
|
||||
* `0.2.0`_ (2012-04-25)
|
||||
* Added Python 3 support.
|
||||
* Added the ability to print the HTTP request as well as the response (see ``--print`` and ``--verbose``).
|
||||
* Added the ability to print the HTTP request as well as the response
|
||||
(see ``--print`` and ``--verbose``).
|
||||
* Added support for Digest authentication.
|
||||
* Added file upload support (``http -f POST file_field_name@/path/to/file``).
|
||||
* Added file upload support
|
||||
(``http -f POST file_field_name@/path/to/file``).
|
||||
* Improved syntax highlighting for JSON.
|
||||
* Added support for field name escaping.
|
||||
* Many bug fixes.
|
||||
* `0.1.6 <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'
|
||||
__version__ = '0.2.5'
|
||||
__version__ = '0.2.6'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -1,130 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
"""The main entry point. Invoke as `http' or `python -m httpie'.
|
||||
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from requests.compat import str
|
||||
|
||||
from . import httpmessage
|
||||
from . import cliparse
|
||||
from . import cli
|
||||
from . import pretty
|
||||
|
||||
|
||||
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
TYPE_JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
def _get_response(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)
|
||||
from .core import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
sys.exit(main())
|
||||
|
120
httpie/cli.py
120
httpie/cli.py
@ -1,20 +1,24 @@
|
||||
"""
|
||||
CLI definition.
|
||||
"""CLI arguments definition.
|
||||
|
||||
NOTE: the CLI interface may change before reaching v1.0.
|
||||
|
||||
"""
|
||||
from . import pretty
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from . import cliparse
|
||||
from .output import AVAILABLE_STYLES
|
||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||
PRETTIFY_STDOUT_TTY_ONLY,
|
||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS)
|
||||
|
||||
|
||||
def _(text):
|
||||
"""Normalize white space."""
|
||||
"""Normalize whitespace."""
|
||||
return ' '.join(text.strip().split())
|
||||
|
||||
|
||||
desc = '%s <http://httpie.org>'
|
||||
parser = cliparse.Parser(description=desc % __doc__.strip(),)
|
||||
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
|
||||
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(
|
||||
'--json', '-j', action='store_true',
|
||||
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
|
||||
are set to application/json (if not specified).
|
||||
''')
|
||||
@ -34,8 +39,10 @@ group_type.add_argument(
|
||||
'--form', '-f', action='store_true',
|
||||
help=_('''
|
||||
Data items from the command line are serialized as form fields.
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not specified).
|
||||
The presence of any file fields results into a multipart/form-data request.
|
||||
The Content-Type is set to application/x-www-form-urlencoded
|
||||
(if not specified).
|
||||
The presence of any file fields results
|
||||
into a multipart/form-data request.
|
||||
''')
|
||||
)
|
||||
|
||||
@ -53,7 +60,7 @@ parser.add_argument(
|
||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
||||
prettify.add_argument(
|
||||
'--pretty', dest='prettify', action='store_true',
|
||||
default=cliparse.PRETTIFY_STDOUT_TTY_ONLY,
|
||||
default=PRETTIFY_STDOUT_TTY_ONLY,
|
||||
help=_('''
|
||||
If stdout is a terminal, the response is prettified
|
||||
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.add_argument('--print', '-p', dest='output_options',
|
||||
default=cliparse.OUT_RESP_HEADERS + cliparse.OUT_RESP_BODY,
|
||||
help=_('''
|
||||
String specifying what should the output contain.
|
||||
"{request_headers}" stands for the request headers and
|
||||
String specifying what the output should contain:
|
||||
"{request_headers}" stands for the request headers, and
|
||||
"{request_body}" for the request body.
|
||||
"{response_headers}" stands for the response headers and
|
||||
"{response_body}" for response the body.
|
||||
Defaults to "hb" which means that the whole response
|
||||
(headers and body) is printed.
|
||||
The default behaviour is "hb" (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
If the output is piped to another program or to a file,
|
||||
then only the body is printed by default.
|
||||
'''.format(
|
||||
request_headers=cliparse.OUT_REQ_HEADERS,
|
||||
request_body=cliparse.OUT_REQ_BODY,
|
||||
response_headers=cliparse.OUT_RESP_HEADERS,
|
||||
response_body=cliparse.OUT_RESP_BODY,
|
||||
request_headers=OUT_REQ_HEAD,
|
||||
request_body=OUT_REQ_BODY,
|
||||
response_headers=OUT_RESP_HEAD,
|
||||
response_body=OUT_RESP_BODY,
|
||||
))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v', dest='output_options',
|
||||
action='store_const', const=''.join(cliparse.OUTPUT_OPTIONS),
|
||||
action='store_const', const=''.join(OUTPUT_OPTIONS),
|
||||
help=_('''
|
||||
Print the whole request as well as the response.
|
||||
Shortcut for --print={0}.
|
||||
'''.format(''.join(cliparse.OUTPUT_OPTIONS)))
|
||||
'''.format(''.join(OUTPUT_OPTIONS)))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--headers', '-t', dest='output_options',
|
||||
action='store_const', const=cliparse.OUT_RESP_HEADERS,
|
||||
'--headers', '-h', dest='output_options',
|
||||
action='store_const', const=OUT_RESP_HEAD,
|
||||
help=_('''
|
||||
Print only the response headers.
|
||||
Shortcut for --print={0}.
|
||||
'''.format(cliparse.OUT_RESP_HEADERS))
|
||||
'''.format(OUT_RESP_HEAD))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b', dest='output_options',
|
||||
action='store_const', const=cliparse.OUT_RESP_BODY,
|
||||
action='store_const', const=OUT_RESP_BODY,
|
||||
help=_('''
|
||||
Print only the response body.
|
||||
Shortcut for --print={0}.
|
||||
'''.format(cliparse.OUT_RESP_BODY))
|
||||
'''.format(OUT_RESP_BODY))
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--style', '-s', dest='style', default='solarized', metavar='STYLE',
|
||||
choices=pretty.AVAILABLE_STYLES,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help=_('''
|
||||
Output coloring style, one of %s. Defaults to solarized.
|
||||
For this option to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
''') % ', '.join(sorted(pretty.AVAILABLE_STYLES))
|
||||
''') % ', '.join(sorted(AVAILABLE_STYLES))
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--check-status', default=False, action='store_true',
|
||||
help=_('''
|
||||
By default, HTTPie exits with 0 when no network or other fatal
|
||||
errors occur.
|
||||
|
||||
This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
|
||||
When the server replies with a 4xx (Client Error) or 5xx
|
||||
(Server Error) status code, HTTPie exits with 4 or 5 respectively.
|
||||
If the response is a 3xx (Redirect) and --allow-redirects
|
||||
hasn't been set, then the exit status is 3.
|
||||
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
''')
|
||||
)
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
parser.add_argument(
|
||||
'--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON),
|
||||
'--auth', '-a',
|
||||
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
||||
help=_('''
|
||||
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(
|
||||
'--auth-type', choices=['basic', 'digest'],
|
||||
help=_('The authentication mechanism to be used. Defaults to "basic".')
|
||||
'--auth-type', choices=['basic', 'digest'], default='basic',
|
||||
help=_('''
|
||||
The authentication mechanism to be used.
|
||||
Defaults to "basic".
|
||||
''')
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
@ -147,7 +179,7 @@ parser.add_argument(
|
||||
)
|
||||
parser.add_argument(
|
||||
'--proxy', default=[], action='append',
|
||||
type=cliparse.KeyValueType(cliparse.SEP_COMMON),
|
||||
type=KeyValueArgType(SEP_PROXY),
|
||||
help=_('''
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:foo.bar:3128).
|
||||
@ -179,8 +211,9 @@ parser.add_argument(
|
||||
help=_('''
|
||||
The HTTP method to be used for the request
|
||||
(GET, POST, PUT, DELETE, PATCH, ...).
|
||||
If this argument is omitted, then HTTPie will guess the HTTP method.
|
||||
If there is some data to be sent, then it will be POST, otherwise GET.
|
||||
If this argument is omitted, then HTTPie
|
||||
will guess the HTTP method. If there is some
|
||||
data to be sent, then it will be POST, otherwise GET.
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
@ -193,18 +226,15 @@ parser.add_argument(
|
||||
parser.add_argument(
|
||||
'items', nargs='*',
|
||||
metavar='ITEM',
|
||||
type=cliparse.KeyValueType(
|
||||
cliparse.SEP_COMMON,
|
||||
cliparse.SEP_DATA,
|
||||
cliparse.SEP_DATA_RAW_JSON,
|
||||
cliparse.SEP_FILES
|
||||
),
|
||||
type=KeyValueArgType(*SEP_GROUP_ITEMS),
|
||||
help=_('''
|
||||
A key-value pair whose type is defined by the separator used. It can be an
|
||||
HTTP header (header:value),
|
||||
A key-value pair whose type is defined by the
|
||||
separator used. It can be an HTTP header (header:value),
|
||||
a data field to be used in the request body (field_name=value),
|
||||
a raw JSON data field (field_name:=value),
|
||||
a query parameter (name==value),
|
||||
or a file field (field_name@/path/to/file).
|
||||
You can use a backslash to escape a colliding separator in the field name.
|
||||
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)):
|
||||
# argparse has been added in Python 3.2 / 2.7
|
||||
requirements.append('argparse>=1.2.1')
|
||||
if 'win32' in str(sys.platform).lower():
|
||||
# Terminal colors for Windows
|
||||
requirements.append('colorama>=0.2.4')
|
||||
|
||||
|
||||
setup(
|
||||
@ -52,5 +55,6 @@ setup(
|
||||
'Topic :: System :: Networking',
|
||||
'Topic :: Terminals',
|
||||
'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 -*-
|
||||
import unittest
|
||||
import argparse
|
||||
#!/usr/bin/env python
|
||||
# coding=utf8
|
||||
"""
|
||||
|
||||
Many of the test cases here use httpbin.org.
|
||||
|
||||
To make it run faster and offline you can::
|
||||
|
||||
# Install `httpbin` locally
|
||||
pip install git+https://github.com/kennethreitz/httpbin.git
|
||||
|
||||
# Run it
|
||||
httpbin
|
||||
|
||||
# Run the tests against it
|
||||
HTTPBIN_URL=http://localhost:5000 python setup.py test
|
||||
|
||||
# Test all Python environments
|
||||
HTTPBIN_URL=http://localhost:5000 tox
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import json
|
||||
from requests.compat import is_py26, is_py3
|
||||
from requests import Response
|
||||
import tempfile
|
||||
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__)
|
||||
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_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
||||
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
||||
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
||||
|
||||
|
||||
def httpbin(path):
|
||||
return HTTPBIN_URL + path
|
||||
|
||||
|
||||
class Response(str):
|
||||
"""
|
||||
A unicode subclass holding the output of `main()`, and also
|
||||
the exit status, the contents of ``stderr``, and de-serialized
|
||||
JSON response (if possible).
|
||||
|
||||
"""
|
||||
exit_status = None
|
||||
stderr = None
|
||||
json = None
|
||||
|
||||
|
||||
def http(*args, **kwargs):
|
||||
"""
|
||||
Invoke `httpie.__main__.main` with `args` and `kwargs`,
|
||||
Invoke `httpie.core.main()` with `args` and `kwargs`,
|
||||
and return a unicode response.
|
||||
|
||||
"""
|
||||
http_kwargs = {
|
||||
'stdin_isatty': True,
|
||||
'stdout_isatty': False
|
||||
}
|
||||
http_kwargs.update(kwargs)
|
||||
stdout = http_kwargs.setdefault('stdout', tempfile.TemporaryFile())
|
||||
__main__.main(args=args, **http_kwargs)
|
||||
|
||||
if 'env' not in kwargs:
|
||||
# Ensure that we have terminal by default (needed for Travis).
|
||||
kwargs['env'] = Environment(
|
||||
colors=0,
|
||||
stdin_isatty=True,
|
||||
stdout_isatty=True,
|
||||
)
|
||||
|
||||
stdout = kwargs['env'].stdout = tempfile.TemporaryFile()
|
||||
stderr = kwargs['env'].stderr = tempfile.TemporaryFile()
|
||||
|
||||
exit_status = main(args=['--traceback'] + list(args), **kwargs)
|
||||
|
||||
stdout.seek(0)
|
||||
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()
|
||||
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):
|
||||
@ -60,47 +131,145 @@ class BaseTestCase(unittest.TestCase):
|
||||
|
||||
|
||||
#################################################################
|
||||
# High-level tests using httpbin.org.
|
||||
# High-level tests using httpbin.
|
||||
#################################################################
|
||||
|
||||
class HTTPieTest(BaseTestCase):
|
||||
|
||||
def test_GET(self):
|
||||
r = http('GET', 'http://httpbin.org/get')
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/get')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
def test_DELETE(self):
|
||||
r = http('DELETE', 'http://httpbin.org/delete')
|
||||
r = http(
|
||||
'DELETE',
|
||||
httpbin('/delete')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
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('"foo": "bar"', r)
|
||||
|
||||
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('"foo": "bar"', r)
|
||||
|
||||
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('"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):
|
||||
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(TEST_FILE_CONTENT, r)
|
||||
|
||||
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('"User-Agent": "HTTPie', r)
|
||||
self.assertIn('"Foo": "bar"', r)
|
||||
|
||||
|
||||
class QuerystringTest(BaseTestCase):
|
||||
|
||||
def test_query_string_params_in_url(self):
|
||||
r = http(
|
||||
'--print=Hhb',
|
||||
'GET',
|
||||
httpbin('/get?a=1&b=2')
|
||||
)
|
||||
|
||||
path = '/get?a=1&b=2'
|
||||
url = httpbin(path)
|
||||
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('GET %s HTTP/1.1' % path, r)
|
||||
self.assertIn('"url": "%s"' % url, r)
|
||||
|
||||
def test_query_string_params_items(self):
|
||||
r = http(
|
||||
'--print=Hhb',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
'a==1',
|
||||
'b==2'
|
||||
)
|
||||
|
||||
path = '/get?a=1&b=2'
|
||||
url = httpbin(path)
|
||||
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('GET %s HTTP/1.1' % path, r)
|
||||
self.assertIn('"url": "%s"' % url, r)
|
||||
|
||||
def test_query_string_params_in_url_and_items_with_duplicates(self):
|
||||
r = http(
|
||||
'--print=Hhb',
|
||||
'GET',
|
||||
httpbin('/get?a=1&a=1'),
|
||||
'a==1',
|
||||
'a==1',
|
||||
'b==2',
|
||||
)
|
||||
|
||||
path = '/get?a=1&a=1&a=1&a=1&b=2'
|
||||
url = httpbin(path)
|
||||
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('GET %s HTTP/1.1' % path, r)
|
||||
self.assertIn('"url": "%s"' % url, r)
|
||||
|
||||
|
||||
class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
|
||||
"""
|
||||
Test that Accept and Content-Type correctly defaults to JSON,
|
||||
@ -110,84 +279,159 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
|
||||
"""
|
||||
def test_GET_no_data_no_auto_headers(self):
|
||||
# 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('"Accept": "*/*"', r)
|
||||
# Although an empty header is present in the response from httpbin,
|
||||
# it's not included in the request.
|
||||
self.assertIn('"Content-Type": ""', r)
|
||||
self.assertNotIn('"Content-Type": "application/json', r)
|
||||
|
||||
def test_POST_no_data_no_auto_headers(self):
|
||||
# JSON headers shouldn't be automatically set for POST with no data.
|
||||
r = http('POST', 'http://httpbin.org/post')
|
||||
r = http(
|
||||
'POST',
|
||||
httpbin('/post')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"Accept": "*/*"', r)
|
||||
# Although an empty header is present in the response from httpbin,
|
||||
# it's not included in the request.
|
||||
self.assertIn(' "Content-Type": ""', r)
|
||||
self.assertNotIn('"Content-Type": "application/json', r)
|
||||
|
||||
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('"Accept": "application/json"', r)
|
||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||
|
||||
def test_GET_with_data_auto_JSON_headers(self):
|
||||
# JSON headers should automatically be set also for GET with data.
|
||||
r = http('POST', 'http://httpbin.org/post', 'a=b')
|
||||
r = http(
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'a=b'
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"Accept": "application/json"', r)
|
||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||
|
||||
def test_POST_explicit_JSON_auto_JSON_headers(self):
|
||||
r = http('-j', 'POST', 'http://httpbin.org/post')
|
||||
r = http(
|
||||
'--json',
|
||||
'POST',
|
||||
httpbin('/post')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"Accept": "application/json"', r)
|
||||
self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
|
||||
|
||||
def test_GET_explicit_JSON_explicit_headers(self):
|
||||
r = http('-j', 'GET', 'http://httpbin.org/headers',
|
||||
'Accept:application/xml',
|
||||
'Content-Type:application/xml')
|
||||
r = http(
|
||||
'--json',
|
||||
'GET',
|
||||
httpbin('/headers'),
|
||||
'Accept:application/xml',
|
||||
'Content-Type:application/xml'
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"Accept": "application/xml"', r)
|
||||
self.assertIn('"Content-Type": "application/xml"', r)
|
||||
|
||||
def test_POST_form_auto_Content_Type(self):
|
||||
r = http('-f', 'POST', 'http://httpbin.org/post')
|
||||
r = http(
|
||||
'--form',
|
||||
'POST',
|
||||
httpbin('/post')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"', r)
|
||||
self.assertIn(
|
||||
'"Content-Type":'
|
||||
' "application/x-www-form-urlencoded; charset=utf-8"',
|
||||
r
|
||||
)
|
||||
|
||||
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('"Content-Type": "application/xml"', r)
|
||||
|
||||
def test_print_only_body_when_stdout_redirected_by_default(self):
|
||||
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=Environment(
|
||||
stdin_isatty=True,
|
||||
stdout_isatty=False
|
||||
)
|
||||
)
|
||||
self.assertNotIn('HTTP/', r)
|
||||
|
||||
def test_print_overridable_when_stdout_redirected(self):
|
||||
|
||||
r = http(
|
||||
'--print=h',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=Environment(
|
||||
stdin_isatty=True,
|
||||
stdout_isatty=False
|
||||
)
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
|
||||
class ImplicitHTTPMethodTest(BaseTestCase):
|
||||
|
||||
def test_implicit_GET(self):
|
||||
r = http('http://httpbin.org/get')
|
||||
r = http(httpbin('/get'))
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
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('"Foo": "bar"', r)
|
||||
|
||||
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('"hello": "world"', r)
|
||||
|
||||
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('"foo": "bar"', r)
|
||||
|
||||
def test_implicit_POST_stdin(self):
|
||||
r = http('--form', 'http://httpbin.org/post',
|
||||
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
|
||||
env = Environment(
|
||||
stdin_isatty=False,
|
||||
stdin=open(TEST_FILE_PATH),
|
||||
stdout_isatty=True,
|
||||
colors=0,
|
||||
)
|
||||
r = http(
|
||||
'--form',
|
||||
httpbin('/post'),
|
||||
env=env
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
|
||||
@ -195,32 +439,66 @@ class PrettyFlagTest(BaseTestCase):
|
||||
"""Test the --pretty / --ugly flag handling."""
|
||||
|
||||
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)
|
||||
|
||||
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
|
||||
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
|
||||
def test_pretty_enabled_by_default_unless_stdout_redirected(self):
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/get')
|
||||
)
|
||||
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
|
||||
|
||||
def test_force_pretty(self):
|
||||
r = http('--pretty', 'GET', '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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class VerboseFlagTest(BaseTestCase):
|
||||
|
||||
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.assertEqual(r.count('__test__'), 2)
|
||||
|
||||
def test_verbose_form(self):
|
||||
# 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('foo=bar&baz=bar', r)
|
||||
|
||||
@ -228,14 +506,22 @@ class VerboseFlagTest(BaseTestCase):
|
||||
class MultipartFormDataFileUploadTest(BaseTestCase):
|
||||
|
||||
def test_non_existent_file_raises_parse_error(self):
|
||||
self.assertRaises(cliparse.ParseError, http,
|
||||
'--form', '--traceback',
|
||||
'POST', 'http://httpbin.org/post',
|
||||
'foo@/__does_not_exist__')
|
||||
self.assertRaises(input.ParseError, http,
|
||||
'--form',
|
||||
'--traceback',
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'foo@/__does_not_exist__'
|
||||
)
|
||||
|
||||
def test_upload_ok(self):
|
||||
r = http('--form', 'POST', 'http://httpbin.org/post',
|
||||
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
|
||||
r = http(
|
||||
'--form',
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'test-file@%s' % TEST_FILE_PATH,
|
||||
'foo=bar'
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"test-file": "%s' % TEST_FILE_CONTENT, r)
|
||||
self.assertIn('"foo": "bar"', r)
|
||||
@ -247,13 +533,22 @@ class RequestBodyFromFilePathTest(BaseTestCase):
|
||||
|
||||
"""
|
||||
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(TEST_FILE_CONTENT, r)
|
||||
self.assertIn('"Content-Type": "text/plain"', r)
|
||||
|
||||
def test_request_body_from_file_by_path_with_explicit_content_type(self):
|
||||
r = http('POST', '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(TEST_FILE_CONTENT, 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):
|
||||
self.assertRaises(SystemExit, lambda: http(
|
||||
'POST',
|
||||
'http://httpbin.org/post',
|
||||
'@' + TEST_FILE_PATH,
|
||||
'@' + TEST_FILE2_PATH))
|
||||
httpbin('/post'),
|
||||
'@' + TEST_FILE_PATH,
|
||||
'@' + TEST_FILE2_PATH)
|
||||
)
|
||||
|
||||
def test_request_body_from_file_by_path_no_data_items_allowed(self):
|
||||
self.assertRaises(SystemExit, lambda: http(
|
||||
'POST',
|
||||
'http://httpbin.org/post',
|
||||
'@' + TEST_FILE_PATH,
|
||||
'foo=bar'))
|
||||
httpbin('/post'),
|
||||
'@' + TEST_FILE_PATH,
|
||||
'foo=bar')
|
||||
)
|
||||
|
||||
|
||||
class AuthTest(BaseTestCase):
|
||||
|
||||
def test_basic_auth(self):
|
||||
r = http('--auth', 'user:password',
|
||||
'GET', 'httpbin.org/basic-auth/user/password')
|
||||
r = http(
|
||||
'--auth=user:password',
|
||||
'GET',
|
||||
httpbin('/basic-auth/user/password')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"authenticated": true', r)
|
||||
self.assertIn('"user": "user"', r)
|
||||
|
||||
def test_digest_auth(self):
|
||||
r = http('--auth-type=digest', '--auth', 'user:password',
|
||||
'GET', 'httpbin.org/digest-auth/auth/user/password')
|
||||
r = http(
|
||||
'--auth-type=digest',
|
||||
'--auth=user:password',
|
||||
'GET',
|
||||
httpbin('/digest-auth/auth/user/password')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"authenticated": true', r)
|
||||
self.assertIn('"user": "user"', r)
|
||||
|
||||
def test_password_prompt(self):
|
||||
cliparse.AuthCredentials._getpass = lambda self, prompt: 'password'
|
||||
|
||||
r = http('--auth', 'user',
|
||||
'GET', 'httpbin.org/basic-auth/user/password')
|
||||
input.AuthCredentials._getpass = lambda self, prompt: 'password'
|
||||
|
||||
r = http(
|
||||
'--auth',
|
||||
'user',
|
||||
'GET',
|
||||
httpbin('/basic-auth/user/password')
|
||||
)
|
||||
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn('"authenticated": true', r)
|
||||
self.assertIn('"user": "user"', r)
|
||||
|
||||
|
||||
class ExitStatusTest(BaseTestCase):
|
||||
|
||||
def test_ok_response_exits_0(self):
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/status/200')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertEqual(r.exit_status, 0)
|
||||
|
||||
def test_error_response_exits_0_without_check_status(self):
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/status/500')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 500', r)
|
||||
self.assertEqual(r.exit_status, 0)
|
||||
|
||||
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
||||
r = http(
|
||||
'--check-status',
|
||||
'--headers', # non-terminal, force headers
|
||||
'GET',
|
||||
httpbin('/status/301'),
|
||||
env=Environment(
|
||||
stdout_isatty=False,
|
||||
stdin_isatty=True,
|
||||
)
|
||||
)
|
||||
self.assertIn('HTTP/1.1 301', r)
|
||||
self.assertEqual(r.exit_status, 3)
|
||||
self.assertIn('301 moved permanently', r.stderr.lower())
|
||||
|
||||
def test_3xx_check_status_redirects_allowed_exits_0(self):
|
||||
r = http(
|
||||
'--check-status',
|
||||
'--allow-redirects',
|
||||
'GET',
|
||||
httpbin('/status/301')
|
||||
)
|
||||
# The redirect will be followed so 200 is expected.
|
||||
self.assertIn('HTTP/1.1 200 OK', r)
|
||||
self.assertEqual(r.exit_status, 0)
|
||||
|
||||
def test_4xx_check_status_exits_4(self):
|
||||
r = http(
|
||||
'--check-status',
|
||||
'GET',
|
||||
httpbin('/status/401')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 401', r)
|
||||
self.assertEqual(r.exit_status, 4)
|
||||
# Also stderr should be empty since stdout isn't redirected.
|
||||
self.assert_(not r.stderr)
|
||||
|
||||
def test_5xx_check_status_exits_5(self):
|
||||
r = http(
|
||||
'--check-status',
|
||||
'GET',
|
||||
httpbin('/status/500')
|
||||
)
|
||||
self.assertIn('HTTP/1.1 500', r)
|
||||
self.assertEqual(r.exit_status, 5)
|
||||
|
||||
|
||||
#################################################################
|
||||
# CLI argument parsing related tests.
|
||||
#################################################################
|
||||
@ -307,11 +681,12 @@ class AuthTest(BaseTestCase):
|
||||
class ItemParsingTest(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.key_value_type = cliparse.KeyValueType(
|
||||
cliparse.SEP_HEADERS,
|
||||
cliparse.SEP_DATA,
|
||||
cliparse.SEP_DATA_RAW_JSON,
|
||||
cliparse.SEP_FILES,
|
||||
self.key_value_type = input.KeyValueArgType(
|
||||
input.SEP_HEADERS,
|
||||
input.SEP_QUERY,
|
||||
input.SEP_DATA,
|
||||
input.SEP_DATA_RAW_JSON,
|
||||
input.SEP_FILES,
|
||||
)
|
||||
|
||||
def test_invalid_items(self):
|
||||
@ -321,7 +696,7 @@ class ItemParsingTest(BaseTestCase):
|
||||
lambda: self.key_value_type(item))
|
||||
|
||||
def test_escape(self):
|
||||
headers, data, files = cliparse.parse_items([
|
||||
headers, data, files, params = input.parse_items([
|
||||
# headers
|
||||
self.key_value_type('foo\\:bar:baz'),
|
||||
self.key_value_type('jack\\@jill:hill'),
|
||||
@ -340,15 +715,15 @@ class ItemParsingTest(BaseTestCase):
|
||||
self.assertIn('bar@baz', files)
|
||||
|
||||
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.assertDictEqual(data, {
|
||||
'bob:=': 'foo',
|
||||
self.assertDictEqual(params, {
|
||||
'bob:': 'foo',
|
||||
})
|
||||
|
||||
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('header:value'),
|
||||
self.key_value_type('list:=["a", 1, {}, false]'),
|
||||
@ -357,6 +732,7 @@ class ItemParsingTest(BaseTestCase):
|
||||
self.key_value_type('ed='),
|
||||
self.key_value_type('bool:=true'),
|
||||
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
|
||||
self.key_value_type('query==value'),
|
||||
])
|
||||
self.assertDictEqual(headers, {
|
||||
'header': 'value',
|
||||
@ -367,7 +743,10 @@ class ItemParsingTest(BaseTestCase):
|
||||
"string": "value",
|
||||
"bool": True,
|
||||
"list": ["a", 1, {}, False],
|
||||
"obj": {"a": "b"}
|
||||
"obj": {"a": "b"},
|
||||
})
|
||||
self.assertDictEqual(params, {
|
||||
'query': 'value',
|
||||
})
|
||||
self.assertIn('test-file', files)
|
||||
|
||||
@ -375,7 +754,7 @@ class ItemParsingTest(BaseTestCase):
|
||||
class ArgumentParserTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.parser = cliparse.Parser()
|
||||
self.parser = input.Parser()
|
||||
|
||||
def test_guess_when_method_set_and_valid(self):
|
||||
args = argparse.Namespace()
|
||||
@ -383,7 +762,7 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
args.url = 'http://example.com/'
|
||||
args.items = []
|
||||
|
||||
self.parser._guess_method(args)
|
||||
self.parser._guess_method(args, Environment())
|
||||
|
||||
self.assertEquals(args.method, 'GET')
|
||||
self.assertEquals(args.url, 'http://example.com/')
|
||||
@ -395,7 +774,10 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
args.url = 'http://example.com/'
|
||||
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.url, 'http://example.com/')
|
||||
@ -407,13 +789,14 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
args.url = 'data=field'
|
||||
args.items = []
|
||||
|
||||
self.parser._guess_method(args)
|
||||
self.parser._guess_method(args, Environment())
|
||||
|
||||
self.assertEquals(args.method, 'POST')
|
||||
self.assertEquals(args.url, 'http://example.com/')
|
||||
self.assertEquals(
|
||||
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):
|
||||
args = argparse.Namespace()
|
||||
@ -421,31 +804,38 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
args.url = 'test:header'
|
||||
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.url, 'http://example.com/')
|
||||
self.assertEquals(
|
||||
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):
|
||||
args = argparse.Namespace()
|
||||
args.method = 'http://example.com/'
|
||||
args.url = 'new_item=a'
|
||||
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, [
|
||||
cliparse.KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
|
||||
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b'),
|
||||
input.KeyValue(
|
||||
key='new_item', value='a', sep='=', orig='new_item=a'),
|
||||
input.KeyValue(key
|
||||
='old_item', value='b', sep='=', orig='old_item=b'),
|
||||
])
|
||||
|
||||
|
||||
class FakeResponse(Response):
|
||||
class FakeResponse(requests.Response):
|
||||
|
||||
class Mock(object):
|
||||
|
||||
@ -489,7 +879,7 @@ class UnicodeOutputTestCase(BaseTestCase):
|
||||
args.style = 'default'
|
||||
|
||||
# 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():
|
||||
self.assertIn(key, output)
|
||||
@ -497,4 +887,5 @@ class UnicodeOutputTestCase(BaseTestCase):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#noinspection PyCallingNonCallable
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user