mirror of
https://github.com/httpie/cli.git
synced 2025-08-12 20:07:04 +02:00
Compare commits
89 Commits
Author | SHA1 | Date | |
---|---|---|---|
d4f2daca56 | |||
d97a610f7c | |||
5cc5b13555 | |||
3043f24733 | |||
093dab5896 | |||
5f42a21cfb | |||
4c45f0d91f | |||
d7ec7b2217 | |||
7817dfbbcc | |||
238b2e0441 | |||
a93d57b58b | |||
79c412064a | |||
0ae9d7af58 | |||
80e317fe24 | |||
1481749c22 | |||
d84d94dd55 | |||
1913b0d438 | |||
fe16f425a9 | |||
7ff71a7f10 | |||
4a37d10245 | |||
e5edb66ae8 | |||
1766dd8291 | |||
675a8b17ad | |||
69e26b8bc8 | |||
291f520e0c | |||
9ec328ff6f | |||
f2d59ba6bd | |||
53caf6ae72 | |||
8175366f27 | |||
8190a7c0c6 | |||
4a615e762f | |||
7426b4b493 | |||
2cdcadd9d5 | |||
18510a9396 | |||
acf5f063c7 | |||
2cf379df78 | |||
dd100c2cc4 | |||
444a9fa929 | |||
4a24cd25b9 | |||
1c5fb89001 | |||
466e1dbedf | |||
d87b2aa0e5 | |||
5d969852c7 | |||
bbc702fa11 | |||
e25d64a610 | |||
a41dd7ac6d | |||
4a6f32a0f4 | |||
548bef7dff | |||
6c2001d1f5 | |||
4029dbf309 | |||
478d654945 | |||
66bdbc3745 | |||
316e3f45a9 | |||
da0eb7db79 | |||
9338aadd75 | |||
dc7d03e6b8 | |||
898408c20c | |||
47de4e2c9c | |||
f74424ef03 | |||
8a9cedb16e | |||
ff9f23da5b | |||
50810e5bd9 | |||
9b586b953b | |||
149cbc1604 | |||
d3df59c8af | |||
2057e13a1d | |||
4957686bcd | |||
4c0d7d526f | |||
0b3bad9c81 | |||
1ed43c1a1e | |||
bf03937f06 | |||
4660da949f | |||
86256af1df | |||
8bf7f8219c | |||
a5522b8233 | |||
b92a3a6d95 | |||
9098e5b6e8 | |||
68640a81b3 | |||
27f08920c4 | |||
c01dd8d64a | |||
76feea2f68 | |||
22a10aec4a | |||
fa334bdf4d | |||
f6724452cf | |||
07de32c406 | |||
1fbe7a6121 | |||
49e44d9b7e | |||
193683afbb | |||
126b1da515 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,4 +6,5 @@ build
|
|||||||
README.html
|
README.html
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov
|
htmlcov
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
@ -3,7 +3,7 @@ python:
|
|||||||
- 2.6
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
- pypy
|
- pypy
|
||||||
- 3.2
|
- 3.3
|
||||||
script: python setup.py test
|
script: python setup.py test
|
||||||
install:
|
install:
|
||||||
- pip install . --use-mirrors
|
- pip install . --use-mirrors
|
||||||
|
@ -8,6 +8,7 @@ HTTPie authors
|
|||||||
Patches and ideas
|
Patches and ideas
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
|
||||||
* `Hank Gay <https://github.com/gthank>`_
|
* `Hank Gay <https://github.com/gthank>`_
|
||||||
* `Jake Basile <https://github.com/jakebasile>`_
|
* `Jake Basile <https://github.com/jakebasile>`_
|
||||||
* `Vladimir Berkutov <https://github.com/dair-targ>`_
|
* `Vladimir Berkutov <https://github.com/dair-targ>`_
|
||||||
|
412
README.rst
412
README.rst
@ -1,12 +1,18 @@
|
|||||||
***********************
|
****************************************
|
||||||
HTTPie: cURL for Humans
|
HTTPie: a CLI, cURL-like tool for humans
|
||||||
***********************
|
****************************************
|
||||||
|
|
||||||
v0.2.7
|
.. image:: https://raw.github.com/claudiatd/httpie-artwork/master/images/httpie_logo_simple.png
|
||||||
|
:alt: HTTPie logo
|
||||||
|
:width: 469
|
||||||
|
:height: 331
|
||||||
|
:align: center
|
||||||
|
|
||||||
HTTPie is a **command line HTTP client** whose goal is to make CLI interaction
|
v0.4.0-alpha (`stable version`_)
|
||||||
with HTTP-based services as **human-friendly** as possible. It provides a
|
|
||||||
simple ``http`` command that allows for sending arbitrary HTTP requests with a
|
HTTPie is a **command line HTTP client**. Its goal is to make CLI interaction
|
||||||
|
with web services as **human-friendly** as possible. It provides a
|
||||||
|
simple ``http`` command that allows for sending arbitrary HTTP requests using a
|
||||||
simple and natural syntax, and displays colorized responses. HTTPie can be used
|
simple and natural syntax, and displays colorized responses. HTTPie can be used
|
||||||
for **testing, debugging**, and generally **interacting** with HTTP servers.
|
for **testing, debugging**, and generally **interacting** with HTTP servers.
|
||||||
|
|
||||||
@ -15,6 +21,7 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
|
|||||||
:alt: HTTPie compared to cURL
|
:alt: HTTPie compared to cURL
|
||||||
:width: 835
|
:width: 835
|
||||||
:height: 835
|
:height: 835
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
|
||||||
HTTPie is written in Python, and under the hood it uses the excellent
|
HTTPie is written in Python, and under the hood it uses the excellent
|
||||||
@ -39,10 +46,11 @@ Main Features
|
|||||||
* Formatted and colorized terminal output
|
* Formatted and colorized terminal output
|
||||||
* Built-in JSON support
|
* Built-in JSON support
|
||||||
* Forms and file uploads
|
* Forms and file uploads
|
||||||
* HTTPS and authorization
|
* HTTPS, proxies, and authentication
|
||||||
* Arbitrary request data
|
* Arbitrary request data
|
||||||
* Custom headers
|
* Custom headers
|
||||||
* Python 2.6 and Python 3 support
|
* Persistent sessions
|
||||||
|
* Python 2.6, 2.7 and 3.x support
|
||||||
* Linux, Mac OS X and Windows support
|
* Linux, Mac OS X and Windows support
|
||||||
* Documentation
|
* Documentation
|
||||||
* Test coverage
|
* Test coverage
|
||||||
@ -58,9 +66,11 @@ or ``easy_install``:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pip install -U httpie
|
$ pip install --upgrade httpie
|
||||||
|
|
||||||
|
|
||||||
|
Alternatively:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ easy_install httpie
|
$ easy_install httpie
|
||||||
@ -76,16 +86,17 @@ Or, you can install the **development version** directly from GitHub:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pip install -U https://github.com/jkbr/httpie/tarball/master
|
$ pip install --upgrade https://github.com/jkbr/httpie/tarball/master
|
||||||
|
|
||||||
|
|
||||||
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
|
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
|
||||||
Linux distributions as well.
|
Linux distributions as well. However, there may be a significant delay between
|
||||||
|
official HTTPie releases and package updates.
|
||||||
|
|
||||||
|
|
||||||
===========
|
=====
|
||||||
Quick Start
|
Usage
|
||||||
===========
|
=====
|
||||||
|
|
||||||
|
|
||||||
Hello World:
|
Hello World:
|
||||||
@ -111,53 +122,64 @@ Examples
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
||||||
Send a ``HEAD`` request:
|
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http HEAD example.org
|
$ http PUT example.org X-API-Token:123 name=John
|
||||||
|
|
||||||
|
|
||||||
Submit a form:
|
Submitting `forms`_:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http --form POST example.org hello=World
|
$ http -f POST example.org hello=World
|
||||||
|
|
||||||
|
|
||||||
Send a ``PUT`` request with a custom header and some JSON data:
|
See the request that is being sent using one of the `output options`_:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http PUT example.org X-API-Token:123 name='David Bowie'
|
$ http -v example.org
|
||||||
|
|
||||||
See the request that is being sent:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ http --verbose example.org
|
|
||||||
|
|
||||||
|
|
||||||
Use `Github API`_ to post a comment on an issue:
|
Use `Github API`_ to post a comment on an issue with `authentication`_:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http -a USERNAME POST https://api.github.com/repos/jkbr/httpie/issues/83/comments body='HTTPie is awesome!'
|
$ http -a USERNAME POST https://api.github.com/repos/jkbr/httpie/issues/83/comments body='HTTPie is awesome!'
|
||||||
|
|
||||||
|
|
||||||
Upload a file:
|
Upload a file using `redirected input`_:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http example.org < file.json
|
$ http example.org < file.json
|
||||||
|
|
||||||
|
|
||||||
Download a file:
|
Download a file and save it via `redirected output`_:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http example.org/file > file
|
$ http example.org/file > file
|
||||||
|
|
||||||
|
Use named `sessions`_ to make certain aspects or the communication persistent
|
||||||
|
between requests to the same host:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=logged-in -a username:password httpbin.org/get API-Key:123
|
||||||
|
|
||||||
|
$ http --session=logged-in httpbin.org/headers
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
--------
|
||||||
|
|
||||||
|
*What follows is a detailed documentation. It covers the command syntax,
|
||||||
|
advanced usage, and also features additional examples.*
|
||||||
|
|
||||||
|
|
||||||
============
|
============
|
||||||
HTTP Method
|
HTTP Method
|
||||||
============
|
============
|
||||||
@ -169,7 +191,7 @@ The name of the HTTP method comes right before the URL argument:
|
|||||||
$ http DELETE example.org/todos/7
|
$ http DELETE example.org/todos/7
|
||||||
|
|
||||||
|
|
||||||
It makes the command look similar to the actual ``Request-Line`` that is sent:
|
Which looks similar to the actual ``Request-Line`` that is sent:
|
||||||
|
|
||||||
.. code-block:: http
|
.. code-block:: http
|
||||||
|
|
||||||
@ -177,29 +199,7 @@ It makes the command look similar to the actual ``Request-Line`` that is sent:
|
|||||||
|
|
||||||
|
|
||||||
When the ``METHOD`` argument is **omitted** from the command, HTTPie defaults to
|
When the ``METHOD`` argument is **omitted** from the command, HTTPie defaults to
|
||||||
either ``GET`` or ``POST``. This depends on whether you are sending
|
either ``GET`` (with no request data) or ``POST`` (with request data).
|
||||||
some data:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ http example.org/todos text='Check out HTTPie'
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
POST /todos HTTP/1.1
|
|
||||||
|
|
||||||
|
|
||||||
, or no data at all:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ http example.org/todos
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
GET /todos HTTP/1.1
|
|
||||||
|
|
||||||
|
|
||||||
===========
|
===========
|
||||||
@ -266,11 +266,12 @@ their type is distinguished only by the separator used:
|
|||||||
+-----------------------+-----------------------------------------------------+
|
+-----------------------+-----------------------------------------------------+
|
||||||
|
|
||||||
You can use ``\`` to escape characters that shouldn't be used as separators
|
You can use ``\`` to escape characters that shouldn't be used as separators
|
||||||
(or parts thereof). e.g., ``foo\==bar`` will become a data key/value
|
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
|
||||||
pair (``foo=`` and ``bar``) instead of a URL parameter.
|
pair (``foo=`` and ``bar``) instead of a URL parameter.
|
||||||
|
|
||||||
No that data fields aren't the only way to specify request data,
|
Note that data fields aren't the only way to specify request data:
|
||||||
`redirected input`_ allows passing arbitrary data to be sent with the request.
|
`Redirected input`_ allows for passing arbitrary data to be sent with the
|
||||||
|
request.
|
||||||
|
|
||||||
|
|
||||||
====
|
====
|
||||||
@ -278,7 +279,7 @@ JSON
|
|||||||
====
|
====
|
||||||
|
|
||||||
JSON is the *lingua franca* of modern web services and it is also the
|
JSON is the *lingua franca* of modern web services and it is also the
|
||||||
**default content type** HTTPie uses:
|
**implicit content type** HTTPie by default uses:
|
||||||
|
|
||||||
If your command includes some data items, they are serialized as a JSON
|
If your command includes some data items, they are serialized as a JSON
|
||||||
object by default. HTTPie also automatically sets the following headers,
|
object by default. HTTPie also automatically sets the following headers,
|
||||||
@ -289,9 +290,9 @@ both of which can be overwritten:
|
|||||||
``Accept`` ``application/json``
|
``Accept`` ``application/json``
|
||||||
================ =======================================
|
================ =======================================
|
||||||
|
|
||||||
You can use ``--json`` / ``-j`` to set ``Accept`` to ``application/json``
|
You can use ``--json`` / ``-j`` to explicitly set ``Accept``
|
||||||
regardless of whether you are sending data (it's a shortcut for using setting
|
to ``application/json`` regardless of whether you are sending data
|
||||||
the header via the usual header notation –
|
(it's a shortcut for setting the header via the usual header notation –
|
||||||
``http url Accept:application/json``).
|
``http url Accept:application/json``).
|
||||||
|
|
||||||
Simple example:
|
Simple example:
|
||||||
@ -342,6 +343,12 @@ into the resulting object:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Send JSON data stored in a file (see `redirected input`_ for more examples):
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http POST api.example.com/person/1 < person.json
|
||||||
|
|
||||||
|
|
||||||
=====
|
=====
|
||||||
Forms
|
Forms
|
||||||
@ -349,9 +356,13 @@ Forms
|
|||||||
|
|
||||||
Submitting forms is very similar to sending `JSON`_ requests. Often the only
|
Submitting forms is very similar to sending `JSON`_ requests. Often the only
|
||||||
difference is in adding the ``--form`` / ``-f`` option, which ensures that
|
difference is in adding the ``--form`` / ``-f`` option, which ensures that
|
||||||
data fields are serialized and ``Content-Type`` is set to
|
data fields are serialized as, and ``Content-Type`` is set to,
|
||||||
``application/x-www-form-urlencoded; charset=utf-8``.
|
``application/x-www-form-urlencoded; charset=utf-8``.
|
||||||
|
|
||||||
|
It is possible to make form data the implicit content type instead of JSON
|
||||||
|
via the `config`_ file.
|
||||||
|
|
||||||
|
|
||||||
-------------
|
-------------
|
||||||
Regular Forms
|
Regular Forms
|
||||||
-------------
|
-------------
|
||||||
@ -374,7 +385,7 @@ Regular Forms
|
|||||||
File Upload Forms
|
File Upload Forms
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
When one or more file fields are present, the content type is
|
If one or more file fields is present, the serialization and content type is
|
||||||
``multipart/form-data``:
|
``multipart/form-data``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
@ -416,8 +427,7 @@ To set custom headers you can use the ``Header:Value`` notation:
|
|||||||
X-Foo: Bar
|
X-Foo: Bar
|
||||||
|
|
||||||
|
|
||||||
There are a couple of default headers that HTTPie sets, but they can easily
|
There are a couple of default headers that HTTPie sets:
|
||||||
be overwritten:
|
|
||||||
|
|
||||||
.. code-block:: http
|
.. code-block:: http
|
||||||
|
|
||||||
@ -428,12 +438,15 @@ be overwritten:
|
|||||||
Host: <taken-from-URL>
|
Host: <taken-from-URL>
|
||||||
|
|
||||||
|
|
||||||
====
|
Any of the default headers can be overwritten.
|
||||||
Auth
|
|
||||||
====
|
|
||||||
|
|
||||||
The currently supported authorization schemes are Basic and Digest (more to
|
|
||||||
come). There are two flags that control authorization:
|
==============
|
||||||
|
Authentication
|
||||||
|
==============
|
||||||
|
|
||||||
|
The currently supported authentication schemes are Basic and Digest (more to
|
||||||
|
come). There are two flags that control authentication:
|
||||||
|
|
||||||
=================== ======================================================
|
=================== ======================================================
|
||||||
``--auth, -a`` Pass a ``username:password`` pair as
|
``--auth, -a`` Pass a ``username:password`` pair as
|
||||||
@ -441,12 +454,16 @@ come). There are two flags that control authorization:
|
|||||||
(``-a username``), you'll be prompted for
|
(``-a username``), you'll be prompted for
|
||||||
the password before the request is sent.
|
the password before the request is sent.
|
||||||
To send a an empty password, pass ``username:``.
|
To send a an empty password, pass ``username:``.
|
||||||
|
The ``username:password@hostname`` URL syntax is
|
||||||
|
supported as well (but credentials passed via ``-a``
|
||||||
|
have higher priority).
|
||||||
|
|
||||||
``--auth-type`` Specify the auth mechanism. Possible values are
|
``--auth-type`` Specify the auth mechanism. Possible values are
|
||||||
``basic`` and ``digest``. The default value is
|
``basic`` and ``digest``. The default value is
|
||||||
``basic`` so it can often be omitted.
|
``basic`` so it can often be omitted.
|
||||||
=================== ======================================================
|
=================== ======================================================
|
||||||
|
|
||||||
|
Authorization information from ``.netrc`` is honored as well.
|
||||||
|
|
||||||
Basic auth:
|
Basic auth:
|
||||||
|
|
||||||
@ -471,6 +488,47 @@ With password prompt:
|
|||||||
$ http -a username example.org
|
$ http -a username example.org
|
||||||
|
|
||||||
|
|
||||||
|
=======
|
||||||
|
Proxies
|
||||||
|
=======
|
||||||
|
|
||||||
|
You can specify proxies to be used through the ``--proxy`` argument:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
|
||||||
|
|
||||||
|
|
||||||
|
With Basic authentication:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
|
||||||
|
|
||||||
|
You can also configure proxies by environment variables ``HTTP_PROXY`` and
|
||||||
|
``HTTPS_PROXY``, and the underlying Requests library will pick them up as well.
|
||||||
|
If you want to disable proxies configured through the environment variables for
|
||||||
|
certain hosts, you can specify them in ``NO_PROXY``.
|
||||||
|
|
||||||
|
In your ``~/.bash_profile``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
export HTTP_PROXY=10.10.1.10:3128
|
||||||
|
export HTTPS_PROXY=10.10.1.10:1080
|
||||||
|
export NO_PROXY=localhost,example.com
|
||||||
|
|
||||||
|
|
||||||
|
=====
|
||||||
|
HTTPS
|
||||||
|
=====
|
||||||
|
|
||||||
|
To skip the host's SSL certificate verification, you can pass ``--verify=no``
|
||||||
|
(default is ``yes``). You can also use ``--verify`` to set a custom CA bundle
|
||||||
|
path. The path can also be configured via the environment variable
|
||||||
|
``REQUESTS_CA_BUNDLE``.
|
||||||
|
|
||||||
|
|
||||||
==============
|
==============
|
||||||
Output Options
|
Output Options
|
||||||
==============
|
==============
|
||||||
@ -530,7 +588,7 @@ Character Stands for
|
|||||||
``b`` Response body.
|
``b`` Response body.
|
||||||
========== ==================
|
========== ==================
|
||||||
|
|
||||||
Print both the request and response headers:
|
Print request and response headers:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@ -547,14 +605,14 @@ request, except that it applies to any HTTP method you use.
|
|||||||
|
|
||||||
Let's say that there is an API that returns the whole resource when it is
|
Let's say that there is an API that returns the whole resource when it is
|
||||||
updated, but you are only interested in the response headers to see the
|
updated, but you are only interested in the response headers to see the
|
||||||
status code after the update:
|
status code after an update:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http --headers PATCH example.org/Really-Huge-Resource name='New Name'
|
$ http --headers PATCH example.org/Really-Huge-Resource name='New Name'
|
||||||
|
|
||||||
|
|
||||||
Since we are only printing the HTTP headers here, the connection to server
|
Since we are only printing the HTTP headers here, the connection to the server
|
||||||
is closed as soon as all the response headers have been received.
|
is closed as soon as all the response headers have been received.
|
||||||
Therefore, bandwidth and time isn't wasted downloading the body
|
Therefore, bandwidth and time isn't wasted downloading the body
|
||||||
which you don't care about.
|
which you don't care about.
|
||||||
@ -603,14 +661,14 @@ You can use ``cat`` to enter multiline data on the terminal:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ cat | http POST example.com⏎
|
$ cat | http POST example.com
|
||||||
<paste>
|
<paste>
|
||||||
^D
|
^D
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ cat | http POST example.com/todos Content-Type:text/plain⏎
|
$ cat | http POST example.com/todos Content-Type:text/plain
|
||||||
- buy milk
|
- buy milk
|
||||||
- call parents
|
- call parents
|
||||||
^D
|
^D
|
||||||
@ -635,7 +693,7 @@ Body Data From a Filename
|
|||||||
``@/path/to/file``) whose content is used as if it came from ``stdin``.
|
``@/path/to/file``) whose content is used as if it came from ``stdin``.
|
||||||
|
|
||||||
It has the advantage that **the** ``Content-Type``
|
It has the advantage that **the** ``Content-Type``
|
||||||
**header will automatically be set** to the appropriate value based on the
|
**header is automatically set** to the appropriate value based on the
|
||||||
filename extension. For example, the following request sends the
|
filename extension. For example, the following request sends the
|
||||||
verbatim contents of that XML file with ``Content-Type: application/xml``:
|
verbatim contents of that XML file with ``Content-Type: application/xml``:
|
||||||
|
|
||||||
@ -648,7 +706,8 @@ verbatim contents of that XML file with ``Content-Type: application/xml``:
|
|||||||
Terminal Output
|
Terminal Output
|
||||||
=================
|
=================
|
||||||
|
|
||||||
HTTPie does several things by default to make its terminal output easy to read.
|
HTTPie does several things by default in order to make its terminal output
|
||||||
|
easy to read.
|
||||||
|
|
||||||
|
|
||||||
---------------------
|
---------------------
|
||||||
@ -656,30 +715,42 @@ Colors and Formatting
|
|||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
Syntax highlighting is applied to HTTP headers and bodies (where it makes
|
Syntax highlighting is applied to HTTP headers and bodies (where it makes
|
||||||
sense). Also, the following formatting is used:
|
sense). You can choose your prefered color scheme via the ``--style`` option
|
||||||
|
if you don't like the default one (see ``$ http --help`` for the possible
|
||||||
|
values).
|
||||||
|
|
||||||
|
Also, the following formatting is applied:
|
||||||
|
|
||||||
* HTTP headers are sorted by name.
|
* HTTP headers are sorted by name.
|
||||||
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||||
to the characters they represent.
|
to the characters they represent.
|
||||||
|
|
||||||
Colorizing and formatting can be disabled with ``--ugly, -u``.
|
One of these options can be used to control output processing:
|
||||||
|
|
||||||
|
==================== ========================================================
|
||||||
|
``--pretty=all`` Apply both colors and formatting.
|
||||||
|
Default for terminal output.
|
||||||
|
``--pretty=colors`` Apply colors.
|
||||||
|
``--pretty=format`` Apply formatting.
|
||||||
|
``--pretty=none`` Disables output processing.
|
||||||
|
Default for redirected output.
|
||||||
|
==================== ========================================================
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
Binary data
|
Binary data
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
Binary data is suppressed for terminal output, which makes it safe to perform
|
Binary data is suppressed for terminal output, which makes it safe to perform
|
||||||
requests to URLs send back binary data. Binary data is suppressed also in
|
requests to URLs that send back binary data. Binary data is suppressed also in
|
||||||
redirected, but prettified output. The connection is closed as soon as we know
|
redirected, but prettified output. The connection is closed as soon as we know
|
||||||
that the response body is binary,
|
that the response body is binary,
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
http example.org/File.mov
|
$ http example.org/Movie.mov
|
||||||
|
|
||||||
|
|
||||||
You will immediately see something like this:
|
You will nearly instantly see something like this:
|
||||||
|
|
||||||
.. code-block:: http
|
.. code-block:: http
|
||||||
|
|
||||||
@ -701,7 +772,7 @@ Redirected Output
|
|||||||
HTTPie uses **different defaults** for redirected output than for
|
HTTPie uses **different defaults** for redirected output than for
|
||||||
`terminal output`_:
|
`terminal output`_:
|
||||||
|
|
||||||
* Formatting and colors aren't applied (unless ``--pretty`` is set).
|
* Formatting and colors aren't applied (unless ``--pretty`` is specified).
|
||||||
* Only the response body is printed (unless one of the `output options`_ is set).
|
* Only the response body is printed (unless one of the `output options`_ is set).
|
||||||
* Also, binary data isn't suppressed.
|
* Also, binary data isn't suppressed.
|
||||||
|
|
||||||
@ -723,12 +794,26 @@ Download an image of Octocat, resize it using ImageMagick, upload it elsewhere:
|
|||||||
$ http octodex.github.com/images/original.jpg | convert - -resize 25% - | http example.org/Octocats
|
$ http octodex.github.com/images/original.jpg | convert - -resize 25% - | http example.org/Octocats
|
||||||
|
|
||||||
|
|
||||||
Force colorizing and formatting, and show both the request and response in
|
Force colorizing and formatting, and show both the request and the response in
|
||||||
``less`` pager:
|
``less`` pager:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ http --pretty --verbose example.org | less -R
|
$ http --pretty=all --verbose example.org | less -R
|
||||||
|
|
||||||
|
|
||||||
|
The ``-R`` flag tells ``less`` to interpret color escape sequences included
|
||||||
|
HTTPie`s output.
|
||||||
|
|
||||||
|
You can create a shortcut for invoking HTTPie with colorized and paged output
|
||||||
|
by adding the following to your ``~/.bash_profile``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
function httpless {
|
||||||
|
# `httpless example.org'
|
||||||
|
http --pretty=all "$@" | less -R;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
==================
|
==================
|
||||||
@ -737,7 +822,7 @@ Streamed Responses
|
|||||||
|
|
||||||
Responses are downloaded and printed in chunks, which allows for streaming
|
Responses are downloaded and printed in chunks, which allows for streaming
|
||||||
and large file downloads without using too much RAM. However, when
|
and large file downloads without using too much RAM. However, when
|
||||||
`colors and formatting`_ are applied, the whole response is buffered and only
|
`colors and formatting`_ is applied, the whole response is buffered and only
|
||||||
then processed at once.
|
then processed at once.
|
||||||
|
|
||||||
|
|
||||||
@ -748,7 +833,7 @@ You can use the ``--stream, -S`` flag to make two things happen:
|
|||||||
|
|
||||||
2. Streaming becomes enabled even when the output is prettified: It will be
|
2. Streaming becomes enabled even when the output is prettified: It will be
|
||||||
applied to **each line** of the response and flushed immediately. This makes
|
applied to **each line** of the response and flushed immediately. This makes
|
||||||
it possible to have a nice output of long-lived requests, such as one
|
it possible to have a nice output for long-lived requests, such as one
|
||||||
to the Twitter streaming API.
|
to the Twitter streaming API.
|
||||||
|
|
||||||
|
|
||||||
@ -759,7 +844,7 @@ Prettified streamed response:
|
|||||||
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track='Justin Bieber'
|
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track='Justin Bieber'
|
||||||
|
|
||||||
|
|
||||||
Streamed output by small chunks:
|
Streamed output by small chunks alá ``tail -f``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@ -769,6 +854,96 @@ Streamed output by small chunks:
|
|||||||
| while read tweet; do echo "$tweet" | http POST example.org/tweets ; done
|
| while read tweet; do echo "$tweet" | http POST example.org/tweets ; done
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
========
|
||||||
|
Sessions
|
||||||
|
========
|
||||||
|
|
||||||
|
By default, every request is completely independent of the previous ones.
|
||||||
|
HTTPie also supports persistent sessions, where custom headers, authorization,
|
||||||
|
and cookies (manually specified or sent by the server) persist between
|
||||||
|
requests to the same host.
|
||||||
|
|
||||||
|
Create a new session named ``user1``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=user1 -a user1:password example.org X-Foo:Bar
|
||||||
|
|
||||||
|
Now you can refer to the session by its name, and the previously used
|
||||||
|
authorization and HTTP headers will automatically be set:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=user1 example.org
|
||||||
|
|
||||||
|
To create or reuse a different session, simple specify a different name:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=user2 -a user2:password example.org X-Bar:Foo
|
||||||
|
|
||||||
|
To use a session without updating it from the request/response exchange
|
||||||
|
once it is created, specify the session name via
|
||||||
|
``--session-read-only=SESSION_NAME`` instead.
|
||||||
|
|
||||||
|
Session data are stored in JSON files in the directory
|
||||||
|
``~/.httpie/sessions/<host>/<name>.json``
|
||||||
|
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||||
|
**Warning:** All session data, including credentials, cookie data,
|
||||||
|
and custom headers are stored in plain text.
|
||||||
|
|
||||||
|
Session files can also be created or edited with a text editor.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ httpie session edit example.org user1
|
||||||
|
|
||||||
|
|
||||||
|
See also `Config`_.
|
||||||
|
|
||||||
|
|
||||||
|
======
|
||||||
|
Config
|
||||||
|
======
|
||||||
|
|
||||||
|
HTTPie uses a simple configuration file that contains a JSON object with the
|
||||||
|
following keys:
|
||||||
|
|
||||||
|
========================= =================================================
|
||||||
|
``__meta__`` HTTPie automatically stores some metadata here.
|
||||||
|
Do not change.
|
||||||
|
|
||||||
|
``implicit_content_type`` A ``String`` specifying the implicit content type
|
||||||
|
for request data. The default value for this
|
||||||
|
option is ``json`` and can be changed to
|
||||||
|
``form``.
|
||||||
|
|
||||||
|
``default_options`` An ``Array`` (by default empty) of options
|
||||||
|
that should be applied to every request.
|
||||||
|
|
||||||
|
For instance, you can use this option to change
|
||||||
|
the default style and output options:
|
||||||
|
``"default_options": ["--style=fruity", "--body"]``
|
||||||
|
|
||||||
|
Another useful default option is
|
||||||
|
``"--session=default"`` to make HTTPie always
|
||||||
|
use `sessions`_.
|
||||||
|
|
||||||
|
Default options from config file can be unset
|
||||||
|
for a particular invocation via
|
||||||
|
``--no-OPTION`` arguments passed on the
|
||||||
|
command line (e.g., ``--no-style``
|
||||||
|
or ``--no-session``).
|
||||||
|
========================= =================================================
|
||||||
|
|
||||||
|
The default location of the configuration file is ``~/.httpie/config.json``
|
||||||
|
(or ``%APPDATA%\httpie\config.json`` on Windows).
|
||||||
|
|
||||||
|
The config directory location can be changed by setting the
|
||||||
|
``HTTPIE_CONFIG_DIR`` environment variable.
|
||||||
|
|
||||||
|
|
||||||
=========
|
=========
|
||||||
Scripting
|
Scripting
|
||||||
=========
|
=========
|
||||||
@ -776,17 +951,19 @@ Scripting
|
|||||||
When using HTTPie from **shell scripts**, it can be handy to set the
|
When using HTTPie from **shell scripts**, it can be handy to set the
|
||||||
``--check-status`` flag. It instructs HTTPie to exit with an error if 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
|
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
|
||||||
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``,
|
be ``3`` (unless ``--follow`` is set), ``4``, or ``5``,
|
||||||
respectively:
|
respectively. Also, the ``--timeout`` option allows to overwrite the default
|
||||||
|
30s timeout:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if http --check-status HEAD example.org/health &> /dev/null; then
|
if http --timeout=2.5 --check-status HEAD example.org/health &> /dev/null; then
|
||||||
echo 'OK!'
|
echo 'OK!'
|
||||||
else
|
else
|
||||||
case $? in
|
case $? in
|
||||||
|
2) echo 'Request timed out!' ;;
|
||||||
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
|
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
|
||||||
4) echo 'HTTP 4xx Client Error!' ;;
|
4) echo 'HTTP 4xx Client Error!' ;;
|
||||||
5) echo 'HTTP 5xx Server Error!' ;;
|
5) echo 'HTTP 5xx Server Error!' ;;
|
||||||
@ -831,10 +1008,15 @@ and that only a small portion of the command is used to control HTTPie and
|
|||||||
doesn't directly correspond to any part of the request (here it's only ``-f``
|
doesn't directly correspond to any part of the request (here it's only ``-f``
|
||||||
asking HTTPie to send a form request).
|
asking HTTPie to send a form request).
|
||||||
|
|
||||||
The two modes, ``--pretty, -p`` (default for terminal) and ``--ugly, -u``
|
The two modes, ``--pretty=all`` (default for terminal) and ``--pretty=none``
|
||||||
(default for redirected output), allow for both user-friendly interactive use
|
(default for redirected output), allow for both user-friendly interactive use
|
||||||
and usage from scripts, where HTTPie serves as a generic HTTP client.
|
and usage from scripts, where HTTPie serves as a generic HTTP client.
|
||||||
|
|
||||||
|
As HTTPie is still under heavy development, the existing command line
|
||||||
|
syntax and some of the ``--OPTIONS`` may change slightly before
|
||||||
|
HTTPie reaches its final version ``1.0``. All changes are recorded in the
|
||||||
|
`changelog`_.
|
||||||
|
|
||||||
|
|
||||||
==========
|
==========
|
||||||
Contribute
|
Contribute
|
||||||
@ -869,7 +1051,7 @@ Please run the existing suite of tests before a pull request is submitted:
|
|||||||
`Tox`_ can also be used to conveniently run tests in all of the
|
`Tox`_ can also be used to conveniently run tests in all of the
|
||||||
`supported Python environments`_:
|
`supported Python environments`_:
|
||||||
|
|
||||||
.. code-b®lock:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# Install tox
|
# Install tox
|
||||||
pip install tox
|
pip install tox
|
||||||
@ -878,8 +1060,13 @@ Please run the existing suite of tests before a pull request is submitted:
|
|||||||
tox
|
tox
|
||||||
|
|
||||||
|
|
||||||
Don't forget to add yourself to `AUTHORS`_.
|
Don't forget to add yourself to `AUTHORS.rst`_.
|
||||||
|
|
||||||
|
=======
|
||||||
|
Logo
|
||||||
|
=======
|
||||||
|
|
||||||
|
See `claudiatd/httpie-artwork`_
|
||||||
|
|
||||||
=======
|
=======
|
||||||
Authors
|
Authors
|
||||||
@ -888,7 +1075,6 @@ Authors
|
|||||||
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
|
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
|
||||||
have contributed.
|
have contributed.
|
||||||
|
|
||||||
|
|
||||||
=======
|
=======
|
||||||
Licence
|
Licence
|
||||||
=======
|
=======
|
||||||
@ -900,14 +1086,36 @@ Please see `LICENSE`_.
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
* `0.2.8dev`_
|
*You can click a version name to see a diff with the previous one.*
|
||||||
|
|
||||||
|
* `0.5.0-alpha`_
|
||||||
|
* `0.4.0`_
|
||||||
|
* Python 3.3 compatibility.
|
||||||
|
* Requests >= v1.0.4 compatibility.
|
||||||
|
* Added support for credentials in URL.
|
||||||
|
* Added ``--no-option`` for every ``--option`` to be config-friendly.
|
||||||
|
* Mutually exclusive arguments can be specified multiple times. The
|
||||||
|
last value is used.
|
||||||
|
* `0.3.0`_ (2012-09-21)
|
||||||
|
* Allow output redirection on Windows.
|
||||||
|
* Added configuration file.
|
||||||
|
* Added persistent session support.
|
||||||
|
* Renamed ``--allow-redirects`` to ``--follow``.
|
||||||
|
* Improved the usability of ``http --help``.
|
||||||
|
* Fixed installation on Windows with Python 3.
|
||||||
|
* Fixed colorized output on Windows with Python 3.
|
||||||
|
* CRLF HTTP header field separation in the output.
|
||||||
|
* Added exit status code ``2`` for timed-out requests.
|
||||||
|
* Added the option to separate colorizing and formatting
|
||||||
|
(``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``).
|
||||||
|
``--ugly`` has bee removed in favor of ``--pretty=none``.
|
||||||
* `0.2.7`_ (2012-08-07)
|
* `0.2.7`_ (2012-08-07)
|
||||||
* Compatibility with Requests 0.13.6.
|
* Compatibility with Requests 0.13.6.
|
||||||
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
|
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
|
||||||
streaming also with ``--pretty`` and to ensure a more frequent output
|
streaming also with ``--pretty`` and to ensure a more frequent output
|
||||||
flushing.
|
flushing.
|
||||||
* Support for efficient large file downloads.
|
* Support for efficient large file downloads.
|
||||||
* Sort headers by name (unless ``--ugly``).
|
* Sort headers by name (unless ``--pretty=none``).
|
||||||
* Response body is fetched only when needed (e.g., not with ``--headers``).
|
* Response body is fetched only when needed (e.g., not with ``--headers``).
|
||||||
* Improved content type matching.
|
* Improved content type matching.
|
||||||
* Updated Solarized color scheme.
|
* Updated Solarized color scheme.
|
||||||
@ -973,6 +1181,7 @@ Changelog
|
|||||||
.. _Jakub Roztocil: http://roztocil.name
|
.. _Jakub Roztocil: http://roztocil.name
|
||||||
.. _@jakubroztocil: https://twitter.com/jakubroztocil
|
.. _@jakubroztocil: https://twitter.com/jakubroztocil
|
||||||
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
|
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
|
||||||
|
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
|
||||||
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
|
.. _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.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.1: https://github.com/jkbr/httpie/compare/0.2.0...0.2.1
|
||||||
@ -980,7 +1189,10 @@ Changelog
|
|||||||
.. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5
|
.. _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.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
|
||||||
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
|
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
|
||||||
.. _0.2.8dev: https://github.com/jkbr/httpie/compare/0.2.7...master
|
.. _0.3.0: https://github.com/jkbr/httpie/compare/0.2.7...0.3.0
|
||||||
.. _README for stable version: https://github.com/jkbr/httpie/tree/0.2.6#readme
|
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
|
||||||
.. _AUTHORS: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
.. _0.5.0-alpha: https://github.com/jkbr/httpie/compare/0.4.0...master
|
||||||
|
.. _stable version: https://github.com/jkbr/httpie/tree/0.3.0#readme
|
||||||
|
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
||||||
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE
|
||||||
|
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
HTTPie - cURL for humans.
|
HTTPie - a CLI, cURL-like tool for humans.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.2.7'
|
__version__ = '0.4.0'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
class EXIT:
|
class ExitStatus:
|
||||||
|
"""Exit status code constants."""
|
||||||
OK = 0
|
OK = 0
|
||||||
ERROR = 1
|
ERROR = 1
|
||||||
# Used only when requested:
|
ERROR_TIMEOUT = 2
|
||||||
|
|
||||||
|
# Used only when requested with --check-status:
|
||||||
ERROR_HTTP_3XX = 3
|
ERROR_HTTP_3XX = 3
|
||||||
ERROR_HTTP_4XX = 4
|
ERROR_HTTP_4XX = 4
|
||||||
ERROR_HTTP_5XX = 5
|
ERROR_HTTP_5XX = 5
|
||||||
|
364
httpie/cli.py
364
httpie/cli.py
@ -1,20 +1,22 @@
|
|||||||
"""CLI arguments definition.
|
"""CLI arguments definition.
|
||||||
|
|
||||||
NOTE: the CLI interface may change before reaching v1.0.
|
NOTE: the CLI interface may change before reaching v1.0.
|
||||||
|
TODO: make the options config friendly, i.e., no mutually exclusive groups to
|
||||||
|
allow options overwriting.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import argparse
|
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||||
|
|
||||||
from requests.compat import is_windows
|
|
||||||
|
|
||||||
from . import __doc__
|
from . import __doc__
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
from .compat import is_windows
|
||||||
|
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
||||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||||
PRETTIFY_STDOUT_TTY_ONLY,
|
|
||||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
||||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY, OUTPUT_OPTIONS)
|
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||||
|
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
||||||
|
|
||||||
|
|
||||||
def _(text):
|
def _(text):
|
||||||
@ -22,15 +24,72 @@ def _(text):
|
|||||||
return ' '.join(text.strip().split())
|
return ' '.join(text.strip().split())
|
||||||
|
|
||||||
|
|
||||||
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
|
parser = Parser(
|
||||||
parser.add_argument('--version', action='version', version=__version__)
|
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||||
|
epilog='For every --option there is a --no-option'
|
||||||
|
' that reverts the option to its default value.\n\n'
|
||||||
|
'Suggestions and bug reports are greatly appreciated:\n'
|
||||||
|
'https://github.com/jkbr/httpie/issues'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Positional arguments.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
positional = parser.add_argument_group(
|
||||||
|
title='Positional arguments',
|
||||||
|
description=_('''
|
||||||
|
These arguments come after any flags and in the
|
||||||
|
order they are listed here. Only URL is required.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
positional.add_argument(
|
||||||
|
'method', metavar='METHOD',
|
||||||
|
nargs=OPTIONAL,
|
||||||
|
default=None,
|
||||||
|
help=_('''
|
||||||
|
The HTTP method to be used for the request
|
||||||
|
(GET, POST, PUT, DELETE, 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.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
positional.add_argument(
|
||||||
|
'url', metavar='URL',
|
||||||
|
help=_('''
|
||||||
|
The protocol defaults to http:// if the
|
||||||
|
URL does not include one.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
positional.add_argument(
|
||||||
|
'items', metavar='REQUEST ITEM',
|
||||||
|
nargs=ZERO_OR_MORE,
|
||||||
|
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 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.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
# Content type.
|
# Content type.
|
||||||
#############################################
|
###############################################################################
|
||||||
|
|
||||||
group_type = parser.add_mutually_exclusive_group(required=False)
|
content_type = parser.add_argument_group(
|
||||||
group_type.add_argument(
|
title='Predefined content types',
|
||||||
|
description=None
|
||||||
|
)
|
||||||
|
|
||||||
|
content_type.add_argument(
|
||||||
'--json', '-j', action='store_true',
|
'--json', '-j', action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
(default) Data items from the command
|
(default) Data items from the command
|
||||||
@ -39,55 +98,66 @@ group_type.add_argument(
|
|||||||
are set to application/json (if not specified).
|
are set to application/json (if not specified).
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
group_type.add_argument(
|
content_type.add_argument(
|
||||||
'--form', '-f', action='store_true',
|
'--form', '-f', action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
Data items from the command line are serialized as form fields.
|
Data items from the command line are serialized as form fields.
|
||||||
The Content-Type is set to application/x-www-form-urlencoded
|
The Content-Type is set to application/x-www-form-urlencoded
|
||||||
(if not specified).
|
(if not specified).
|
||||||
The presence of any file fields results
|
The presence of any file fields results
|
||||||
into a multipart/form-data request.
|
in a multipart/form-data request.
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Output options.
|
###############################################################################
|
||||||
#############################################
|
# Output processing
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
output_processing = parser.add_argument_group(title='Output processing')
|
||||||
|
|
||||||
parser.add_argument(
|
output_processing.add_argument(
|
||||||
'--output', '-o', type=argparse.FileType('w+b'),
|
'--output', '-o', type=FileType('w+b'),
|
||||||
metavar='FILE',
|
metavar='FILE',
|
||||||
help= argparse.SUPPRESS if not is_windows else _(
|
help=SUPPRESS if not is_windows else _(
|
||||||
'''
|
'''
|
||||||
Save output to FILE.
|
Save output to FILE.
|
||||||
This option is a replacement for piping output to FILE,
|
This option is a replacement for piping output to FILE,
|
||||||
which would on Windows result into corrupted data
|
which would on Windows result in corrupted data
|
||||||
being saved.
|
being saved.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
output_processing.add_argument(
|
||||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
'--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY,
|
||||||
prettify.add_argument(
|
choices=sorted(PRETTY_MAP.keys()),
|
||||||
'--pretty', dest='prettify', action='store_true',
|
|
||||||
default=PRETTIFY_STDOUT_TTY_ONLY,
|
|
||||||
help=_('''
|
help=_('''
|
||||||
If stdout is a terminal, the response is prettified
|
Controls output processing. The value can be "none" to not prettify
|
||||||
by default (colorized and indented if it is JSON).
|
the output (default for redirected output), "all" to apply both colors
|
||||||
This flag ensures prettifying even when stdout is redirected.
|
and formatting
|
||||||
|
(default for terminal output), "colors", or "format".
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
prettify.add_argument(
|
output_processing.add_argument(
|
||||||
'--ugly', '-u', dest='prettify', action='store_false',
|
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
|
||||||
|
choices=AVAILABLE_STYLES,
|
||||||
help=_('''
|
help=_('''
|
||||||
Do not prettify the response.
|
Output coloring style. One of %s. Defaults to "%s".
|
||||||
''')
|
For this option to work properly, please make sure that the
|
||||||
|
$TERM environment variable is set to "xterm-256color" or similar
|
||||||
|
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||||
|
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
|
||||||
)
|
)
|
||||||
|
|
||||||
output_options = parser.add_mutually_exclusive_group(required=False)
|
|
||||||
output_options.add_argument('--print', '-p', dest='output_options',
|
###############################################################################
|
||||||
|
# Output options
|
||||||
|
###############################################################################
|
||||||
|
output_options = parser.add_argument_group(title='Output options')
|
||||||
|
|
||||||
|
output_options.add_argument(
|
||||||
|
'--print', '-p', dest='output_options', metavar='WHAT',
|
||||||
help=_('''
|
help=_('''
|
||||||
String specifying what the output should contain:
|
String specifying what the output should contain:
|
||||||
"{request_headers}" stands for the request headers, and
|
"{request_headers}" stands for the request headers, and
|
||||||
@ -98,12 +168,10 @@ output_options.add_argument('--print', '-p', dest='output_options',
|
|||||||
headers and body is printed), if standard output is not redirected.
|
headers and body is printed), if standard output is not redirected.
|
||||||
If the output is piped to another program or to a file,
|
If the output is piped to another program or to a file,
|
||||||
then only the body is printed by default.
|
then only the body is printed by default.
|
||||||
'''.format(
|
'''.format(request_headers=OUT_REQ_HEAD,
|
||||||
request_headers=OUT_REQ_HEAD,
|
request_body=OUT_REQ_BODY,
|
||||||
request_body=OUT_REQ_BODY,
|
response_headers=OUT_RESP_HEAD,
|
||||||
response_headers=OUT_RESP_HEAD,
|
response_body=OUT_RESP_BODY,))
|
||||||
response_body=OUT_RESP_BODY,
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--verbose', '-v', dest='output_options',
|
'--verbose', '-v', dest='output_options',
|
||||||
@ -130,19 +198,9 @@ output_options.add_argument(
|
|||||||
'''.format(OUT_RESP_BODY))
|
'''.format(OUT_RESP_BODY))
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
output_options.add_argument(
|
||||||
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
|
'--stream', '-S', action='store_true', default=False,
|
||||||
choices=AVAILABLE_STYLES,
|
|
||||||
help=_('''
|
help=_('''
|
||||||
Output coloring style, one of %s. Defaults to "%s".
|
|
||||||
For this option to work properly, please make sure that the
|
|
||||||
$TERM environment variable is set to "xterm-256color" or similar
|
|
||||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
|
||||||
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument('--stream', '-S', action='store_true', default=False, help=_(
|
|
||||||
'''
|
|
||||||
Always stream the output by line, i.e., behave like `tail -f'.
|
Always stream the output by line, i.e., behave like `tail -f'.
|
||||||
|
|
||||||
Without --stream and with --pretty (either set or implied),
|
Without --stream and with --pretty (either set or implied),
|
||||||
@ -154,9 +212,100 @@ parser.add_argument('--stream', '-S', action='store_true', default=False, help=_
|
|||||||
It is useful also without --pretty: It ensures that the output is flushed
|
It is useful also without --pretty: It ensures that the output is flushed
|
||||||
more often and in smaller chunks.
|
more often and in smaller chunks.
|
||||||
|
|
||||||
'''
|
''')
|
||||||
))
|
)
|
||||||
parser.add_argument(
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Sessions
|
||||||
|
###############################################################################
|
||||||
|
sessions = parser.add_argument_group(title='Sessions')\
|
||||||
|
.add_mutually_exclusive_group(required=False)
|
||||||
|
|
||||||
|
sessions.add_argument(
|
||||||
|
'--session', metavar='SESSION_NAME', type=RegexValidator(
|
||||||
|
Session.VALID_NAME_PATTERN,
|
||||||
|
'Session name contains invalid characters.'
|
||||||
|
),
|
||||||
|
help=_('''
|
||||||
|
Create, or reuse and update a session.
|
||||||
|
Within a session, custom headers, auth credential, as well as any
|
||||||
|
cookies sent by the server persist between requests.
|
||||||
|
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
|
||||||
|
''' % DEFAULT_SESSIONS_DIR)
|
||||||
|
)
|
||||||
|
sessions.add_argument(
|
||||||
|
'--session-read-only', metavar='SESSION_NAME',
|
||||||
|
help=_('''
|
||||||
|
Create or read a session without updating it form the
|
||||||
|
request/response exchange.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Authentication
|
||||||
|
###############################################################################
|
||||||
|
# ``requests.request`` keyword arguments.
|
||||||
|
auth = parser.add_argument_group(title='Authentication')
|
||||||
|
auth.add_argument(
|
||||||
|
'--auth', '-a', metavar='USER[:PASS]',
|
||||||
|
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
||||||
|
help=_('''
|
||||||
|
If only the username is provided (-a username),
|
||||||
|
HTTPie will prompt for the password.
|
||||||
|
'''),
|
||||||
|
)
|
||||||
|
|
||||||
|
auth.add_argument(
|
||||||
|
'--auth-type', choices=['basic', 'digest'], default='basic',
|
||||||
|
help=_('''
|
||||||
|
The authentication mechanism to be used.
|
||||||
|
Defaults to "basic".
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Network
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
network = parser.add_argument_group(title='Network')
|
||||||
|
|
||||||
|
network.add_argument(
|
||||||
|
'--proxy', default=[], action='append', metavar='PROTOCOL:HOST',
|
||||||
|
type=KeyValueArgType(SEP_PROXY),
|
||||||
|
help=_('''
|
||||||
|
String mapping protocol to the URL of the proxy
|
||||||
|
(e.g. http:foo.bar:3128). You can specify multiple
|
||||||
|
proxies with different protocols.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
network.add_argument(
|
||||||
|
'--follow', default=False, action='store_true',
|
||||||
|
help=_('''
|
||||||
|
Set this flag if full redirects are allowed
|
||||||
|
(e.g. re-POST-ing of data at new ``Location``)
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
network.add_argument(
|
||||||
|
'--verify', default='yes',
|
||||||
|
help=_('''
|
||||||
|
Set to "no" to skip checking the host\'s SSL certificate.
|
||||||
|
You can also pass the path to a CA_BUNDLE
|
||||||
|
file for private certs. You can also set
|
||||||
|
the REQUESTS_CA_BUNDLE environment variable.
|
||||||
|
Defaults to "yes".
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
|
||||||
|
network.add_argument(
|
||||||
|
'--timeout', type=float, default=30, metavar='SECONDS',
|
||||||
|
help=_('''
|
||||||
|
The connection timeout of the request in seconds.
|
||||||
|
The default value is 30 seconds.
|
||||||
|
''')
|
||||||
|
)
|
||||||
|
network.add_argument(
|
||||||
'--check-status', default=False, action='store_true',
|
'--check-status', default=False, action='store_true',
|
||||||
help=_('''
|
help=_('''
|
||||||
By default, HTTPie exits with 0 when no network or other fatal
|
By default, HTTPie exits with 0 when no network or other fatal
|
||||||
@ -167,7 +316,7 @@ parser.add_argument(
|
|||||||
|
|
||||||
When the server replies with a 4xx (Client Error) or 5xx
|
When the server replies with a 4xx (Client Error) or 5xx
|
||||||
(Server Error) status code, HTTPie exits with 4 or 5 respectively.
|
(Server Error) status code, HTTPie exits with 4 or 5 respectively.
|
||||||
If the response is a 3xx (Redirect) and --allow-redirects
|
If the response is a 3xx (Redirect) and --follow
|
||||||
hasn't been set, then the exit status is 3.
|
hasn't been set, then the exit status is 3.
|
||||||
|
|
||||||
Also an error message is written to stderr if stdout is redirected.
|
Also an error message is written to stderr if stdout is redirected.
|
||||||
@ -175,100 +324,29 @@ parser.add_argument(
|
|||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
# ``requests.request`` keyword arguments.
|
|
||||||
parser.add_argument(
|
|
||||||
'--auth', '-a',
|
|
||||||
type=AuthCredentialsArgType(SEP_CREDENTIALS),
|
|
||||||
help=_('''
|
|
||||||
username:password.
|
|
||||||
If only the username is provided (-a username),
|
|
||||||
HTTPie will prompt for the password.
|
|
||||||
'''),
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
###############################################################################
|
||||||
'--auth-type', choices=['basic', 'digest'], default='basic',
|
# Troubleshooting
|
||||||
help=_('''
|
###############################################################################
|
||||||
The authentication mechanism to be used.
|
|
||||||
Defaults to "basic".
|
|
||||||
''')
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
troubleshooting = parser.add_argument_group(title='Troubleshooting')
|
||||||
'--verify', default='yes',
|
|
||||||
help=_('''
|
troubleshooting.add_argument(
|
||||||
Set to "no" to skip checking the host\'s SSL certificate.
|
'--help',
|
||||||
You can also pass the path to a CA_BUNDLE
|
action='help', default=SUPPRESS,
|
||||||
file for private certs. You can also set
|
help='Show this help message and exit'
|
||||||
the REQUESTS_CA_BUNDLE environment variable.
|
|
||||||
Defaults to "yes".
|
|
||||||
''')
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--proxy', default=[], action='append',
|
'--version', action='version', version=__version__)
|
||||||
type=KeyValueArgType(SEP_PROXY),
|
troubleshooting.add_argument(
|
||||||
help=_('''
|
'--traceback', action='store_true', default=False,
|
||||||
String mapping protocol to the URL of the proxy
|
help='Prints exception traceback should one occur.'
|
||||||
(e.g. http:foo.bar:3128).
|
|
||||||
''')
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--allow-redirects', default=False, action='store_true',
|
|
||||||
help=_('''
|
|
||||||
Set this flag if full redirects are allowed
|
|
||||||
(e.g. re-POST-ing of data at new ``Location``)
|
|
||||||
''')
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--timeout', type=float,
|
|
||||||
help=_('''
|
|
||||||
Float describes the timeout of the request
|
|
||||||
(Use socket.setdefaulttimeout() as fallback).
|
|
||||||
''')
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--debug', action='store_true', default=False,
|
'--debug', action='store_true', default=False,
|
||||||
help=_('''
|
help=_('''
|
||||||
Prints exception traceback should one occur and other
|
Prints exception traceback should one occur, and also other
|
||||||
information useful for debugging HTTPie itself.
|
information that is useful for debugging HTTPie itself and
|
||||||
''')
|
for bug reports.
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Positional arguments.
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'method', metavar='METHOD',
|
|
||||||
nargs='?',
|
|
||||||
default=None,
|
|
||||||
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.
|
|
||||||
''')
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'url', metavar='URL',
|
|
||||||
help=_('''
|
|
||||||
The protocol defaults to http:// if the
|
|
||||||
URL does not include one.
|
|
||||||
''')
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'items', nargs='*',
|
|
||||||
metavar='ITEM',
|
|
||||||
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 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.
|
|
||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
94
httpie/client.py
Normal file
94
httpie/client.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import requests.auth
|
||||||
|
|
||||||
|
from . import sessions
|
||||||
|
from . import __version__
|
||||||
|
|
||||||
|
|
||||||
|
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||||
|
JSON = 'application/json; charset=utf-8'
|
||||||
|
DEFAULT_UA = 'HTTPie/%s' % __version__
|
||||||
|
|
||||||
|
|
||||||
|
def get_response(args, config_dir):
|
||||||
|
"""Send the request and return a `request.Response`."""
|
||||||
|
|
||||||
|
requests_kwargs = get_requests_kwargs(args)
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
sys.stderr.write('\n>>> requests.request(%s)\n\n'
|
||||||
|
% pformat(requests_kwargs))
|
||||||
|
|
||||||
|
if not args.session and not args.session_read_only:
|
||||||
|
response = requests.request(**requests_kwargs)
|
||||||
|
else:
|
||||||
|
response = sessions.get_response(
|
||||||
|
config_dir=config_dir,
|
||||||
|
name=args.session or args.session_read_only,
|
||||||
|
request_kwargs=requests_kwargs,
|
||||||
|
read_only=bool(args.session_read_only),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_requests_kwargs(args):
|
||||||
|
"""Translate our `args` into `requests.request` keyword arguments."""
|
||||||
|
|
||||||
|
implicit_headers = {
|
||||||
|
'User-Agent': DEFAULT_UA
|
||||||
|
}
|
||||||
|
|
||||||
|
auto_json = args.data and not args.form
|
||||||
|
if args.json or auto_json:
|
||||||
|
implicit_headers['Accept'] = 'application/json'
|
||||||
|
if args.data:
|
||||||
|
implicit_headers['Content-Type'] = JSON
|
||||||
|
|
||||||
|
if isinstance(args.data, dict):
|
||||||
|
if args.data:
|
||||||
|
args.data = json.dumps(args.data)
|
||||||
|
else:
|
||||||
|
# We need to set data to an empty string to prevent requests
|
||||||
|
# from assigning an empty list to `response.request.data`.
|
||||||
|
args.data = ''
|
||||||
|
|
||||||
|
elif args.form and not args.files:
|
||||||
|
# If sending files, `requests` will set
|
||||||
|
# the `Content-Type` for us.
|
||||||
|
implicit_headers['Content-Type'] = FORM
|
||||||
|
|
||||||
|
for name, value in implicit_headers.items():
|
||||||
|
if name not in args.headers:
|
||||||
|
args.headers[name] = value
|
||||||
|
|
||||||
|
credentials = None
|
||||||
|
if args.auth:
|
||||||
|
credentials = {
|
||||||
|
'basic': requests.auth.HTTPBasicAuth,
|
||||||
|
'digest': requests.auth.HTTPDigestAuth,
|
||||||
|
}[args.auth_type](args.auth.key, args.auth.value)
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'stream': True,
|
||||||
|
'method': args.method.lower(),
|
||||||
|
'url': 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.follow,
|
||||||
|
'params': args.params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return kwargs
|
18
httpie/compat.py
Normal file
18
httpie/compat.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Python 2/3 compatibility.
|
||||||
|
|
||||||
|
"""
|
||||||
|
#noinspection PyUnresolvedReferences
|
||||||
|
from requests.compat import (
|
||||||
|
is_windows,
|
||||||
|
bytes,
|
||||||
|
str,
|
||||||
|
is_py3,
|
||||||
|
is_py26,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
except ImportError:
|
||||||
|
from urlparse import urlsplit
|
||||||
|
|
95
httpie/config.py
Normal file
95
httpie/config.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import errno
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
|
from .compat import is_windows
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_DIR = os.environ.get(
|
||||||
|
'HTTPIE_CONFIG_DIR',
|
||||||
|
os.path.expanduser('~/.httpie') if not is_windows else
|
||||||
|
os.path.expandvars(r'%APPDATA%\\httpie')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConfigDict(dict):
|
||||||
|
|
||||||
|
name = None
|
||||||
|
help = None
|
||||||
|
about = None
|
||||||
|
|
||||||
|
directory = DEFAULT_CONFIG_DIR
|
||||||
|
|
||||||
|
def __init__(self, directory=None, *args, **kwargs):
|
||||||
|
super(BaseConfigDict, self).__init__(*args, **kwargs)
|
||||||
|
if directory:
|
||||||
|
self.directory = directory
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
return self[item]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
try:
|
||||||
|
os.makedirs(self.directory, mode=0o700)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
return os.path.join(self.directory, self.name + '.json')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_new(self):
|
||||||
|
return not os.path.exists(self.path)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
try:
|
||||||
|
with open(self.path, 'rt') as f:
|
||||||
|
try:
|
||||||
|
data = json.load(f)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(
|
||||||
|
'Invalid %s JSON: %s [%s]' %
|
||||||
|
(type(self).__name__, e.message, self.path)
|
||||||
|
)
|
||||||
|
self.update(data)
|
||||||
|
except IOError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
self['__meta__'] = {
|
||||||
|
'httpie': __version__
|
||||||
|
}
|
||||||
|
if self.help:
|
||||||
|
self['__meta__']['help'] = self.help
|
||||||
|
|
||||||
|
if self.about:
|
||||||
|
self['__meta__']['about'] = self.about
|
||||||
|
|
||||||
|
with open(self.path, 'w') as f:
|
||||||
|
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
|
||||||
|
f.write('\n')
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
try:
|
||||||
|
os.unlink(self.path)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseConfigDict):
|
||||||
|
|
||||||
|
name = 'config'
|
||||||
|
help = 'https://github.com/jkbr/httpie#config'
|
||||||
|
about = 'HTTPie configuration file'
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
'implicit_content_type': 'json',
|
||||||
|
'default_options': []
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Config, self).__init__(*args, **kwargs)
|
||||||
|
self.update(self.DEFAULTS)
|
157
httpie/core.py
157
httpie/core.py
@ -11,134 +11,119 @@ Invocation flow:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import requests.auth
|
from httpie import __version__ as httpie_version
|
||||||
from requests.compat import str
|
from requests import __version__ as requests_version
|
||||||
|
from pygments import __version__ as pygments_version
|
||||||
|
|
||||||
from .cli import parser
|
from .cli import parser
|
||||||
|
from .compat import str, is_py3
|
||||||
|
from .client import get_response
|
||||||
from .models import Environment
|
from .models import Environment
|
||||||
from .output import output_stream, write
|
from .output import build_output_stream, write, write_with_colors_win_p3k
|
||||||
from . import EXIT
|
from . import ExitStatus
|
||||||
|
|
||||||
|
|
||||||
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
def get_exit_status(http_status, follow=False):
|
||||||
JSON = 'application/json; charset=utf-8'
|
"""Translate HTTP status code to exit status code."""
|
||||||
|
if 300 <= http_status <= 399 and not follow:
|
||||||
|
|
||||||
def get_response(args):
|
|
||||||
"""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 and args.data:
|
|
||||||
args.headers['Content-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'] = FORM
|
|
||||||
|
|
||||||
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(
|
|
||||||
prefetch=False,
|
|
||||||
method=args.method.lower(),
|
|
||||||
url=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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exist_status(code, allow_redirects=False):
|
|
||||||
"""Translate HTTP status code to exit status."""
|
|
||||||
if 300 <= code <= 399 and not allow_redirects:
|
|
||||||
# Redirect
|
# Redirect
|
||||||
return EXIT.ERROR_HTTP_3XX
|
return ExitStatus.ERROR_HTTP_3XX
|
||||||
elif 400 <= code <= 499:
|
elif 400 <= http_status <= 499:
|
||||||
# Client Error
|
# Client Error
|
||||||
return EXIT.ERROR_HTTP_4XX
|
return ExitStatus.ERROR_HTTP_4XX
|
||||||
elif 500 <= code <= 599:
|
elif 500 <= http_status <= 599:
|
||||||
# Server Error
|
# Server Error
|
||||||
return EXIT.ERROR_HTTP_5XX
|
return ExitStatus.ERROR_HTTP_5XX
|
||||||
else:
|
else:
|
||||||
return EXIT.OK
|
return ExitStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
def print_debug_info(env):
|
||||||
|
sys.stderr.writelines([
|
||||||
|
'HTTPie %s\n' % httpie_version,
|
||||||
|
'HTTPie data: %s\n' % env.config.directory,
|
||||||
|
'Requests %s\n' % requests_version,
|
||||||
|
'Pygments %s\n' % pygments_version,
|
||||||
|
'Python %s %s\n' % (sys.version, sys.platform)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def main(args=sys.argv[1:], env=Environment()):
|
def main(args=sys.argv[1:], env=Environment()):
|
||||||
"""Run the main program and write the output to ``env.stdout``.
|
"""Run the main program and write the output to ``env.stdout``.
|
||||||
|
|
||||||
Return exit status.
|
Return exit status code.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if env.config.default_options:
|
||||||
|
args = env.config.default_options + args
|
||||||
|
|
||||||
def error(msg, *args):
|
def error(msg, *args, **kwargs):
|
||||||
msg = msg % args
|
msg = msg % args
|
||||||
env.stderr.write('\nhttp: error: %s\n' % msg)
|
level = kwargs.get('level', 'error')
|
||||||
|
env.stderr.write('http: %s: %s\n' % (level, msg))
|
||||||
|
|
||||||
debug = '--debug' in args
|
debug = '--debug' in args
|
||||||
status = EXIT.OK
|
traceback = debug or '--traceback' in args
|
||||||
|
exit_status = ExitStatus.OK
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print_debug_info(env)
|
||||||
|
if args == ['--debug']:
|
||||||
|
return exit_status
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = parser.parse_args(args=args, env=env)
|
args = parser.parse_args(args=args, env=env)
|
||||||
response = get_response(args)
|
|
||||||
|
response = get_response(args, config_dir=env.config.directory)
|
||||||
|
|
||||||
if args.check_status:
|
if args.check_status:
|
||||||
status = get_exist_status(response.status_code,
|
exit_status = get_exit_status(response.status_code, args.follow)
|
||||||
args.allow_redirects)
|
|
||||||
if status and not env.stdout_isatty:
|
|
||||||
error('%s %s', response.raw.status, response.raw.reason)
|
|
||||||
|
|
||||||
stream = output_stream(args, env, response.request, response)
|
if not env.stdout_isatty and exit_status != ExitStatus.OK:
|
||||||
|
error('HTTP %s %s',
|
||||||
|
response.raw.status,
|
||||||
|
response.raw.reason,
|
||||||
|
level='warning')
|
||||||
|
|
||||||
|
write_kwargs = {
|
||||||
|
'stream': build_output_stream(args, env,
|
||||||
|
response.request,
|
||||||
|
response),
|
||||||
|
'outfile': env.stdout,
|
||||||
|
'flush': env.stdout_isatty or args.stream
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
write(stream=stream,
|
if env.is_windows and is_py3 and 'colors' in args.prettify:
|
||||||
outfile=env.stdout,
|
write_with_colors_win_p3k(**write_kwargs)
|
||||||
flush=env.stdout_isatty or args.stream)
|
else:
|
||||||
|
write(**write_kwargs)
|
||||||
|
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
if not debug and e.errno == errno.EPIPE:
|
if not traceback and e.errno == errno.EPIPE:
|
||||||
# Ignore broken pipes unless --debug.
|
# Ignore broken pipes unless --traceback.
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
if debug:
|
if traceback:
|
||||||
raise
|
raise
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
status = EXIT.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
|
|
||||||
|
except requests.Timeout:
|
||||||
|
exit_status = ExitStatus.ERROR_TIMEOUT
|
||||||
|
error('Request timed out (%ss).', args.timeout)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: distinguish between expected and unexpected errors.
|
# TODO: Better distinction between expected and unexpected errors.
|
||||||
# network errors vs. bugs, etc.
|
# Network errors vs. bugs, etc.
|
||||||
if debug:
|
if traceback:
|
||||||
raise
|
raise
|
||||||
error('%s: %s', type(e).__name__, str(e))
|
error('%s: %s', type(e).__name__, str(e))
|
||||||
status = EXIT.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
|
|
||||||
return status
|
return exit_status
|
||||||
|
145
httpie/input.py
145
httpie/input.py
@ -5,20 +5,21 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import argparse
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import getpass
|
import getpass
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError:
|
except ImportError:
|
||||||
OrderedDict = dict
|
OrderedDict = dict
|
||||||
|
|
||||||
|
# TODO: Use MultiDict for headers once added to `requests`.
|
||||||
|
# https://github.com/jkbr/httpie/issues/130
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
from requests.compat import str, urlparse
|
|
||||||
|
|
||||||
from . import __version__
|
from .compat import urlsplit, str
|
||||||
|
|
||||||
|
|
||||||
HTTP_POST = 'POST'
|
HTTP_POST = 'POST'
|
||||||
@ -66,15 +67,22 @@ OUTPUT_OPTIONS = frozenset([
|
|||||||
OUT_RESP_BODY
|
OUT_RESP_BODY
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Pretty
|
||||||
|
PRETTY_MAP = {
|
||||||
|
'all': ['format', 'colors'],
|
||||||
|
'colors': ['colors'],
|
||||||
|
'format': ['format'],
|
||||||
|
'none': []
|
||||||
|
}
|
||||||
|
PRETTY_STDOUT_TTY_ONLY = object()
|
||||||
|
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = 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):
|
class Parser(ArgumentParser):
|
||||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||||
|
|
||||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||||
@ -85,27 +93,29 @@ class Parser(argparse.ArgumentParser):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['add_help'] = False
|
kwargs['add_help'] = False
|
||||||
super(Parser, self).__init__(*args, **kwargs)
|
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
|
#noinspection PyMethodOverriding
|
||||||
def parse_args(self, env, args=None, namespace=None):
|
def parse_args(self, env, args=None, namespace=None):
|
||||||
|
|
||||||
self.env = env
|
self.env = env
|
||||||
|
|
||||||
if env.is_windows and not env.stdout_isatty:
|
args, no_options = super(Parser, self).parse_known_args(args,
|
||||||
self.error('Output redirection is not supported on Windows.'
|
namespace)
|
||||||
' Please use `--output FILE\' instead.')
|
|
||||||
|
|
||||||
args = super(Parser, self).parse_args(args, namespace)
|
self._apply_no_options(args, no_options)
|
||||||
|
|
||||||
|
if not args.json and env.config.implicit_content_type == 'form':
|
||||||
|
args.form = True
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
args.traceback = True
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
env.stdout = args.output
|
env.stdout = args.output
|
||||||
env.stdout_isatty = False
|
env.stdout_isatty = False
|
||||||
|
|
||||||
self._process_output_options(args, env)
|
self._process_output_options(args, env)
|
||||||
|
self._process_pretty_options(args, env)
|
||||||
self._guess_method(args, env)
|
self._guess_method(args, env)
|
||||||
self._parse_items(args)
|
self._parse_items(args)
|
||||||
|
|
||||||
@ -116,17 +126,54 @@ class Parser(argparse.ArgumentParser):
|
|||||||
scheme = HTTPS if env.progname == 'https' else HTTP
|
scheme = HTTPS if env.progname == 'https' else HTTP
|
||||||
args.url = scheme + args.url
|
args.url = scheme + args.url
|
||||||
|
|
||||||
if args.auth and not args.auth.has_password():
|
self._process_auth(args)
|
||||||
# Stdin already read (if not a tty) so it's save to prompt.
|
|
||||||
args.auth.prompt_password(urlparse(args.url).netloc)
|
|
||||||
|
|
||||||
if args.prettify == PRETTIFY_STDOUT_TTY_ONLY:
|
|
||||||
args.prettify = env.stdout_isatty
|
|
||||||
elif args.prettify and env.is_windows:
|
|
||||||
self.error('Only terminal output can be prettified on Windows.')
|
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
def _process_auth(self, args):
|
||||||
|
url = urlsplit(args.url)
|
||||||
|
|
||||||
|
if args.auth:
|
||||||
|
if not args.auth.has_password():
|
||||||
|
# Stdin already read (if not a tty) so it's save to prompt.
|
||||||
|
args.auth.prompt_password(url.netloc)
|
||||||
|
|
||||||
|
elif url.username is not None:
|
||||||
|
# Handle http://username:password@hostname/
|
||||||
|
username, password = url.username, url.password
|
||||||
|
args.auth = AuthCredentials(
|
||||||
|
key=username,
|
||||||
|
value=password,
|
||||||
|
sep=SEP_CREDENTIALS,
|
||||||
|
orig=SEP_CREDENTIALS.join([username, password])
|
||||||
|
)
|
||||||
|
|
||||||
|
def _apply_no_options(self, args, no_options):
|
||||||
|
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||||
|
its default value. This allows for un-setting of options, e.g.,
|
||||||
|
specified in config.
|
||||||
|
|
||||||
|
"""
|
||||||
|
invalid = []
|
||||||
|
|
||||||
|
for option in no_options:
|
||||||
|
if not option.startswith('--no-'):
|
||||||
|
invalid.append(option)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --no-option => --option
|
||||||
|
inverted = '--' + option[5:]
|
||||||
|
for action in self._actions:
|
||||||
|
if inverted in action.option_strings:
|
||||||
|
setattr(args, action.dest, action.default)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
invalid.append(option)
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
msg = 'unrecognized arguments: %s'
|
||||||
|
self.error(msg % ' '.join(invalid))
|
||||||
|
|
||||||
def _print_message(self, message, file=None):
|
def _print_message(self, message, file=None):
|
||||||
# Sneak in our stderr/stdout.
|
# Sneak in our stderr/stdout.
|
||||||
file = {
|
file = {
|
||||||
@ -170,8 +217,8 @@ class Parser(argparse.ArgumentParser):
|
|||||||
args.items.insert(
|
args.items.insert(
|
||||||
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
|
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
|
||||||
|
|
||||||
except argparse.ArgumentTypeError as e:
|
except ArgumentTypeError as e:
|
||||||
if args.debug:
|
if args.traceback:
|
||||||
raise
|
raise
|
||||||
self.error(e.message)
|
self.error(e.message)
|
||||||
|
|
||||||
@ -189,7 +236,6 @@ class Parser(argparse.ArgumentParser):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
args.headers = CaseInsensitiveDict()
|
args.headers = CaseInsensitiveDict()
|
||||||
args.headers['User-Agent'] = DEFAULT_UA
|
|
||||||
args.data = ParamDict() if args.form else OrderedDict()
|
args.data = ParamDict() if args.form else OrderedDict()
|
||||||
args.files = OrderedDict()
|
args.files = OrderedDict()
|
||||||
args.params = ParamDict()
|
args.params = ParamDict()
|
||||||
@ -201,7 +247,7 @@ class Parser(argparse.ArgumentParser):
|
|||||||
files=args.files,
|
files=args.files,
|
||||||
params=args.params)
|
params=args.params)
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
if args.debug:
|
if args.traceback:
|
||||||
raise
|
raise
|
||||||
self.error(e.message)
|
self.error(e.message)
|
||||||
|
|
||||||
@ -238,6 +284,14 @@ class Parser(argparse.ArgumentParser):
|
|||||||
if unknown:
|
if unknown:
|
||||||
self.error('Unknown output options: %s' % ','.join(unknown))
|
self.error('Unknown output options: %s' % ','.join(unknown))
|
||||||
|
|
||||||
|
def _process_pretty_options(self, args, env):
|
||||||
|
if args.prettify == PRETTY_STDOUT_TTY_ONLY:
|
||||||
|
args.prettify = PRETTY_MAP['all' if env.stdout_isatty else 'none']
|
||||||
|
elif args.prettify and env.is_windows:
|
||||||
|
self.error('Only terminal output can be colorized on Windows.')
|
||||||
|
else:
|
||||||
|
args.prettify = PRETTY_MAP[args.prettify]
|
||||||
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
class ParseError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -256,6 +310,38 @@ class KeyValue(object):
|
|||||||
return self.__dict__ == other.__dict__
|
return self.__dict__ == other.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
def session_name_arg_type(name):
|
||||||
|
from .sessions import Session
|
||||||
|
if not Session.is_valid_name(name):
|
||||||
|
raise ArgumentTypeError(
|
||||||
|
'special characters and spaces are not'
|
||||||
|
' allowed in session names: "%s"'
|
||||||
|
% name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def host_name_arg_type(name):
|
||||||
|
from .sessions import Host
|
||||||
|
if not Host.is_valid_name(name):
|
||||||
|
raise ArgumentTypeError(
|
||||||
|
'special characters and spaces are not'
|
||||||
|
' allowed in host names: "%s"'
|
||||||
|
% name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class RegexValidator(object):
|
||||||
|
|
||||||
|
def __init__(self, pattern, error_message):
|
||||||
|
self.pattern = re.compile(pattern)
|
||||||
|
self.error_message = error_message
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
if not self.pattern.search(value):
|
||||||
|
raise ArgumentError(None, self.error_message)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class KeyValueArgType(object):
|
class KeyValueArgType(object):
|
||||||
"""A key-value pair argument type used with `argparse`.
|
"""A key-value pair argument type used with `argparse`.
|
||||||
|
|
||||||
@ -336,7 +422,7 @@ class KeyValueArgType(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise argparse.ArgumentTypeError(
|
raise ArgumentTypeError(
|
||||||
'"%s" is not a valid value' % string)
|
'"%s" is not a valid value' % string)
|
||||||
|
|
||||||
return self.key_value_class(
|
return self.key_value_class(
|
||||||
@ -375,7 +461,7 @@ class AuthCredentialsArgType(KeyValueArgType):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return super(AuthCredentialsArgType, self).__call__(string)
|
return super(AuthCredentialsArgType, self).__call__(string)
|
||||||
except argparse.ArgumentTypeError:
|
except ArgumentTypeError:
|
||||||
# No password provided, will prompt for it later.
|
# No password provided, will prompt for it later.
|
||||||
return self.key_value_class(
|
return self.key_value_class(
|
||||||
key=string,
|
key=string,
|
||||||
@ -397,9 +483,6 @@ class ParamDict(OrderedDict):
|
|||||||
data and URL params.
|
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:
|
if key not in self:
|
||||||
super(ParamDict, self).__setitem__(key, value)
|
super(ParamDict, self).__setitem__(key, value)
|
||||||
else:
|
else:
|
||||||
|
105
httpie/models.py
105
httpie/models.py
@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from requests.compat import urlparse, is_windows, bytes, str
|
|
||||||
|
from .config import DEFAULT_CONFIG_DIR, Config
|
||||||
|
from .compat import urlsplit, is_windows, bytes, str
|
||||||
|
|
||||||
|
|
||||||
class Environment(object):
|
class Environment(object):
|
||||||
@ -11,21 +13,24 @@ class Environment(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#noinspection PyUnresolvedReferences
|
|
||||||
is_windows = is_windows
|
is_windows = is_windows
|
||||||
|
|
||||||
progname = os.path.basename(sys.argv[0])
|
progname = os.path.basename(sys.argv[0])
|
||||||
if progname not in ['http', 'https']:
|
if progname not in ['http', 'https']:
|
||||||
progname = 'http'
|
progname = 'http'
|
||||||
|
|
||||||
if is_windows:
|
|
||||||
import colorama.initialise
|
|
||||||
colorama.initialise.init()
|
|
||||||
|
|
||||||
stdin_isatty = sys.stdin.isatty()
|
stdin_isatty = sys.stdin.isatty()
|
||||||
stdin = sys.stdin
|
stdin = sys.stdin
|
||||||
stdout_isatty = sys.stdout.isatty()
|
stdout_isatty = sys.stdout.isatty()
|
||||||
stdout = sys.stdout
|
|
||||||
|
config_dir = DEFAULT_CONFIG_DIR
|
||||||
|
|
||||||
|
if stdout_isatty and is_windows:
|
||||||
|
from colorama.initialise import wrap_stream
|
||||||
|
stdout = wrap_stream(sys.stdout, convert=None,
|
||||||
|
strip=None, autoreset=True, wrap=True)
|
||||||
|
else:
|
||||||
|
stdout = sys.stdout
|
||||||
stderr = sys.stderr
|
stderr = sys.stderr
|
||||||
|
|
||||||
# Can be set to 0 to disable colors completely.
|
# Can be set to 0 to disable colors completely.
|
||||||
@ -36,6 +41,16 @@ class Environment(object):
|
|||||||
for attr in kwargs.keys())
|
for attr in kwargs.keys())
|
||||||
self.__dict__.update(**kwargs)
|
self.__dict__.update(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
if not hasattr(self, '_config'):
|
||||||
|
self._config = Config(directory=self.config_dir)
|
||||||
|
if self._config.is_new:
|
||||||
|
self._config.save()
|
||||||
|
else:
|
||||||
|
self._config.load()
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
|
||||||
class HTTPMessage(object):
|
class HTTPMessage(object):
|
||||||
"""Abstract class for HTTP messages."""
|
"""Abstract class for HTTP messages."""
|
||||||
@ -82,19 +97,28 @@ class HTTPResponse(HTTPMessage):
|
|||||||
return self._orig.iter_content(chunk_size=chunk_size)
|
return self._orig.iter_content(chunk_size=chunk_size)
|
||||||
|
|
||||||
def iter_lines(self, chunk_size):
|
def iter_lines(self, chunk_size):
|
||||||
for line in self._orig.iter_lines(chunk_size):
|
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
|
||||||
yield line, b'\n'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
def headers(self):
|
||||||
original = self._orig.raw._original_response
|
original = self._orig.raw._original_response
|
||||||
status_line = 'HTTP/{version} {status} {reason}'.format(
|
status_line = 'HTTP/{version} {status} {reason}'.format(
|
||||||
version='.'.join(str(original.version)),
|
version='.'.join(str(original.version)),
|
||||||
status=original.status,
|
status=original.status,
|
||||||
reason=original.reason
|
reason=original.reason
|
||||||
)
|
)
|
||||||
headers = str(original.msg)
|
headers = [status_line]
|
||||||
return '\n'.join([status_line, headers]).strip()
|
try:
|
||||||
|
# `original.msg` is a `http.client.HTTPMessage` on Python 3
|
||||||
|
# `_headers` is a 2-tuple
|
||||||
|
headers.extend(
|
||||||
|
'%s: %s' % header for header in original.msg._headers)
|
||||||
|
except AttributeError:
|
||||||
|
# and a `httplib.HTTPMessage` on Python 2.x
|
||||||
|
# `headers` is a list of `name: val<CRLF>`.
|
||||||
|
headers.extend(h.strip() for h in original.msg.headers)
|
||||||
|
|
||||||
|
return '\r\n'.join(headers)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encoding(self):
|
def encoding(self):
|
||||||
@ -118,40 +142,25 @@ class HTTPRequest(HTTPMessage):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self):
|
def headers(self):
|
||||||
"""Return Request-Line"""
|
url = urlsplit(self._orig.url)
|
||||||
url = urlparse(self._orig.url)
|
|
||||||
|
|
||||||
# Querystring
|
|
||||||
qs = ''
|
|
||||||
if url.query or self._orig.params:
|
|
||||||
qs = '?'
|
|
||||||
if url.query:
|
|
||||||
qs += url.query
|
|
||||||
# Requests doesn't make params part of ``request.url``.
|
|
||||||
if self._orig.params:
|
|
||||||
if url.query:
|
|
||||||
qs += '&'
|
|
||||||
#noinspection PyUnresolvedReferences
|
|
||||||
qs += type(self._orig)._encode_params(self._orig.params)
|
|
||||||
|
|
||||||
# Request-Line
|
|
||||||
request_line = '{method} {path}{query} HTTP/1.1'.format(
|
request_line = '{method} {path}{query} HTTP/1.1'.format(
|
||||||
method=self._orig.method,
|
method=self._orig.method,
|
||||||
path=url.path or '/',
|
path=url.path or '/',
|
||||||
query=qs
|
query='?' + url.query if url.query else ''
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = dict(self._orig.headers)
|
headers = dict(self._orig.headers)
|
||||||
|
|
||||||
if 'Host' not in headers:
|
if 'Host' not in headers:
|
||||||
headers['Host'] = urlparse(self._orig.url).netloc
|
headers['Host'] = url.netloc
|
||||||
|
|
||||||
headers = ['%s: %s' % (name, value)
|
headers = ['%s: %s' % (name, value)
|
||||||
for name, value in headers.items()]
|
for name, value in headers.items()]
|
||||||
|
|
||||||
headers.insert(0, request_line)
|
headers.insert(0, request_line)
|
||||||
|
|
||||||
return '\n'.join(headers).strip()
|
return '\r\n'.join(headers).strip()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encoding(self):
|
def encoding(self):
|
||||||
@ -159,26 +168,8 @@ class HTTPRequest(HTTPMessage):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def body(self):
|
def body(self):
|
||||||
"""Reconstruct and return the original request body bytes."""
|
body = self._orig.body
|
||||||
if self._orig.files:
|
if isinstance(body, str):
|
||||||
# TODO: would be nice if we didn't need to encode the files again
|
# Happens with JSON/form request data parsed from the command line.
|
||||||
# FIXME: Also the boundary header doesn't match the one used.
|
body = body.encode('utf8')
|
||||||
for fn, fd in self._orig.files.values():
|
return body or b''
|
||||||
# Rewind the files as they have already been read before.
|
|
||||||
fd.seek(0)
|
|
||||||
body, _ = self._orig._encode_files(self._orig.files)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
body = self._orig.data
|
|
||||||
except AttributeError:
|
|
||||||
# requests < 0.12.1
|
|
||||||
body = self._orig._enc_data
|
|
||||||
|
|
||||||
if isinstance(body, dict):
|
|
||||||
#noinspection PyUnresolvedReferences
|
|
||||||
body = type(self._orig)._encode_params(body)
|
|
||||||
|
|
||||||
if isinstance(body, str):
|
|
||||||
body = body.encode('utf8')
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
170
httpie/output.py
170
httpie/output.py
@ -12,21 +12,20 @@ from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
|
|||||||
from pygments.formatters.terminal import TerminalFormatter
|
from pygments.formatters.terminal import TerminalFormatter
|
||||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
from requests.compat import is_windows
|
|
||||||
|
|
||||||
|
from .compat import is_windows
|
||||||
from .solarized import Solarized256Style
|
from .solarized import Solarized256Style
|
||||||
from .models import HTTPRequest, HTTPResponse, Environment
|
from .models import HTTPRequest, HTTPResponse, Environment
|
||||||
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||||
|
|
||||||
|
|
||||||
# Colors on Windows via colorama aren't that great and fruity
|
# Colors on Windows via colorama don't look that
|
||||||
# seems to give the best result there.
|
# great and fruity seems to give the best result there.
|
||||||
|
AVAILABLE_STYLES = set(STYLE_MAP.keys())
|
||||||
|
AVAILABLE_STYLES.add('solarized')
|
||||||
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
|
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
|
||||||
|
|
||||||
#noinspection PySetFunctionToLiteral
|
|
||||||
AVAILABLE_STYLES = set([DEFAULT_STYLE]) | set(STYLE_MAP.keys())
|
|
||||||
|
|
||||||
|
|
||||||
BINARY_SUPPRESSED_NOTICE = (
|
BINARY_SUPPRESSED_NOTICE = (
|
||||||
b'\n'
|
b'\n'
|
||||||
@ -62,23 +61,38 @@ def write(stream, outfile, flush):
|
|||||||
outfile.flush()
|
outfile.flush()
|
||||||
|
|
||||||
|
|
||||||
def output_stream(args, env, request, response):
|
def write_with_colors_win_p3k(stream, outfile, flush):
|
||||||
|
"""Like `write`, but colorized chunks are written as text
|
||||||
|
directly to `outfile` to ensure it gets processed by colorama.
|
||||||
|
Applies only to Windows with Python 3 and colorized terminal output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
color = b'\x1b['
|
||||||
|
encoding = outfile.encoding
|
||||||
|
for chunk in stream:
|
||||||
|
if color in chunk:
|
||||||
|
outfile.write(chunk.decode(encoding))
|
||||||
|
else:
|
||||||
|
outfile.buffer.write(chunk)
|
||||||
|
if flush:
|
||||||
|
outfile.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def build_output_stream(args, env, request, response):
|
||||||
"""Build and return a chain of iterators over the `request`-`response`
|
"""Build and return a chain of iterators over the `request`-`response`
|
||||||
exchange each of which yields `bytes` chunks.
|
exchange each of which yields `bytes` chunks.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Stream = make_stream(env, args)
|
|
||||||
|
|
||||||
req_h = OUT_REQ_HEAD in args.output_options
|
req_h = OUT_REQ_HEAD in args.output_options
|
||||||
req_b = OUT_REQ_BODY in args.output_options
|
req_b = OUT_REQ_BODY in args.output_options
|
||||||
resp_h = OUT_RESP_HEAD in args.output_options
|
resp_h = OUT_RESP_HEAD in args.output_options
|
||||||
resp_b = OUT_RESP_BODY in args.output_options
|
resp_b = OUT_RESP_BODY in args.output_options
|
||||||
|
|
||||||
req = req_h or req_b
|
req = req_h or req_b
|
||||||
resp = resp_h or resp_b
|
resp = resp_h or resp_b
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
|
Stream = get_stream_type(env, args)
|
||||||
|
|
||||||
if req:
|
if req:
|
||||||
output.append(Stream(
|
output.append(Stream(
|
||||||
@ -86,8 +100,9 @@ def output_stream(args, env, request, response):
|
|||||||
with_headers=req_h,
|
with_headers=req_h,
|
||||||
with_body=req_b))
|
with_body=req_b))
|
||||||
|
|
||||||
if req and resp:
|
if req_b and resp:
|
||||||
output.append([b'\n\n\n'])
|
# Request/Response separator.
|
||||||
|
output.append([b'\n\n'])
|
||||||
|
|
||||||
if resp:
|
if resp:
|
||||||
output.append(Stream(
|
output.append(Stream(
|
||||||
@ -95,13 +110,15 @@ def output_stream(args, env, request, response):
|
|||||||
with_headers=resp_h,
|
with_headers=resp_h,
|
||||||
with_body=resp_b))
|
with_body=resp_b))
|
||||||
|
|
||||||
if env.stdout_isatty:
|
if env.stdout_isatty and resp_b:
|
||||||
|
# Ensure a blank line after the response body.
|
||||||
|
# For terminal output only.
|
||||||
output.append([b'\n\n'])
|
output.append([b'\n\n'])
|
||||||
|
|
||||||
return chain(*output)
|
return chain(*output)
|
||||||
|
|
||||||
|
|
||||||
def make_stream(env, args):
|
def get_stream_type(env, args):
|
||||||
"""Pick the right stream type based on `env` and `args`.
|
"""Pick the right stream type based on `env` and `args`.
|
||||||
Wrap it in a partial with the type-specific args so that
|
Wrap it in a partial with the type-specific args so that
|
||||||
we don't need to think what stream we are dealing with.
|
we don't need to think what stream we are dealing with.
|
||||||
@ -112,12 +129,15 @@ def make_stream(env, args):
|
|||||||
RawStream,
|
RawStream,
|
||||||
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
||||||
if args.stream
|
if args.stream
|
||||||
else RawStream.CHUNK_SIZE)
|
else RawStream.CHUNK_SIZE
|
||||||
|
)
|
||||||
elif args.prettify:
|
elif args.prettify:
|
||||||
Stream = partial(
|
Stream = partial(
|
||||||
PrettyStream if args.stream else BufferedPrettyStream,
|
PrettyStream if args.stream else BufferedPrettyStream,
|
||||||
processor=OutputProcessor(env, pygments_style=args.style),
|
env=env,
|
||||||
env=env)
|
processor=OutputProcessor(
|
||||||
|
env=env, groups=args.prettify, pygments_style=args.style),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
Stream = partial(EncodedStream, env=env)
|
Stream = partial(EncodedStream, env=env)
|
||||||
|
|
||||||
@ -125,7 +145,7 @@ def make_stream(env, args):
|
|||||||
|
|
||||||
|
|
||||||
class BaseStream(object):
|
class BaseStream(object):
|
||||||
"""Base HTTP message stream class."""
|
"""Base HTTP message output stream class."""
|
||||||
|
|
||||||
def __init__(self, msg, with_headers=True, with_body=True):
|
def __init__(self, msg, with_headers=True, with_body=True):
|
||||||
"""
|
"""
|
||||||
@ -134,37 +154,29 @@ class BaseStream(object):
|
|||||||
:param with_body: if `True`, body will be included
|
:param with_body: if `True`, body will be included
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
assert with_headers or with_body
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.with_headers = with_headers
|
self.with_headers = with_headers
|
||||||
self.with_body = with_body
|
self.with_body = with_body
|
||||||
|
|
||||||
def _headers(self):
|
def _get_headers(self):
|
||||||
"""Return the headers' bytes."""
|
"""Return the headers' bytes."""
|
||||||
return self.msg.headers.encode('ascii')
|
return self.msg.headers.encode('ascii')
|
||||||
|
|
||||||
def _body(self):
|
def _iter_body(self):
|
||||||
"""Return an iterator over the message body."""
|
"""Return an iterator over the message body."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Return an iterator over `self.msg`."""
|
"""Return an iterator over `self.msg`."""
|
||||||
if self.with_headers:
|
if self.with_headers:
|
||||||
yield self._headers()
|
yield self._get_headers()
|
||||||
|
yield b'\r\n\r\n'
|
||||||
|
|
||||||
if self.with_body:
|
if self.with_body:
|
||||||
it = self._body()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.with_headers:
|
for chunk in self._iter_body():
|
||||||
# Yield the headers/body separator only if needed.
|
|
||||||
chunk = next(it)
|
|
||||||
if chunk:
|
|
||||||
yield b'\n\n'
|
|
||||||
yield chunk
|
|
||||||
|
|
||||||
for chunk in it:
|
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
except BinarySuppressedError as e:
|
except BinarySuppressedError as e:
|
||||||
if self.with_headers:
|
if self.with_headers:
|
||||||
yield b'\n'
|
yield b'\n'
|
||||||
@ -181,7 +193,7 @@ class RawStream(BaseStream):
|
|||||||
super(RawStream, self).__init__(**kwargs)
|
super(RawStream, self).__init__(**kwargs)
|
||||||
self.chunk_size = chunk_size
|
self.chunk_size = chunk_size
|
||||||
|
|
||||||
def _body(self):
|
def _iter_body(self):
|
||||||
return self.msg.iter_body(self.chunk_size)
|
return self.msg.iter_body(self.chunk_size)
|
||||||
|
|
||||||
|
|
||||||
@ -194,6 +206,7 @@ class EncodedStream(BaseStream):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
CHUNK_SIZE = 1024 * 5
|
CHUNK_SIZE = 1024 * 5
|
||||||
|
|
||||||
def __init__(self, env=Environment(), **kwargs):
|
def __init__(self, env=Environment(), **kwargs):
|
||||||
|
|
||||||
super(EncodedStream, self).__init__(**kwargs)
|
super(EncodedStream, self).__init__(**kwargs)
|
||||||
@ -208,7 +221,7 @@ class EncodedStream(BaseStream):
|
|||||||
# Default to utf8 when unsure.
|
# Default to utf8 when unsure.
|
||||||
self.output_encoding = output_encoding or 'utf8'
|
self.output_encoding = output_encoding or 'utf8'
|
||||||
|
|
||||||
def _body(self):
|
def _iter_body(self):
|
||||||
|
|
||||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||||
|
|
||||||
@ -234,11 +247,11 @@ class PrettyStream(EncodedStream):
|
|||||||
super(PrettyStream, self).__init__(**kwargs)
|
super(PrettyStream, self).__init__(**kwargs)
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
|
|
||||||
def _headers(self):
|
def _get_headers(self):
|
||||||
return self.processor.process_headers(
|
return self.processor.process_headers(
|
||||||
self.msg.headers).encode(self.output_encoding)
|
self.msg.headers).encode(self.output_encoding)
|
||||||
|
|
||||||
def _body(self):
|
def _iter_body(self):
|
||||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||||
if b'\0' in line:
|
if b'\0' in line:
|
||||||
raise BinarySuppressedError()
|
raise BinarySuppressedError()
|
||||||
@ -262,9 +275,8 @@ class BufferedPrettyStream(PrettyStream):
|
|||||||
|
|
||||||
CHUNK_SIZE = 1024 * 10
|
CHUNK_SIZE = 1024 * 10
|
||||||
|
|
||||||
def _body(self):
|
def _iter_body(self):
|
||||||
|
|
||||||
#noinspection PyArgumentList
|
|
||||||
# Read the whole body before prettifying it,
|
# Read the whole body before prettifying it,
|
||||||
# but bail out immediately if the body is binary.
|
# but bail out immediately if the body is binary.
|
||||||
body = bytearray()
|
body = bytearray()
|
||||||
@ -297,13 +309,13 @@ class HTTPLexer(lexer.RegexLexer):
|
|||||||
# Request-Line
|
# Request-Line
|
||||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||||
lexer.bygroups(
|
lexer.bygroups(
|
||||||
token.Name.Function,
|
token.Name.Function,
|
||||||
token.Text,
|
token.Text,
|
||||||
token.Name.Namespace,
|
token.Name.Namespace,
|
||||||
token.Text,
|
token.Text,
|
||||||
token.Keyword.Reserved,
|
token.Keyword.Reserved,
|
||||||
token.Operator,
|
token.Operator,
|
||||||
token.Number
|
token.Number
|
||||||
)),
|
)),
|
||||||
# Response Status-Line
|
# Response Status-Line
|
||||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||||
@ -318,13 +330,14 @@ class HTTPLexer(lexer.RegexLexer):
|
|||||||
)),
|
)),
|
||||||
# Header
|
# Header
|
||||||
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
|
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
|
||||||
token.Name.Attribute, # Name
|
token.Name.Attribute, # Name
|
||||||
token.Text,
|
token.Text,
|
||||||
token.Operator, # Colon
|
token.Operator, # Colon
|
||||||
token.Text,
|
token.Text,
|
||||||
token.String # Value
|
token.String # Value
|
||||||
))
|
))
|
||||||
]}
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseProcessor(object):
|
class BaseProcessor(object):
|
||||||
@ -332,12 +345,11 @@ class BaseProcessor(object):
|
|||||||
|
|
||||||
enabled = True
|
enabled = True
|
||||||
|
|
||||||
def __init__(self, env, **kwargs):
|
def __init__(self, env=Environment(), **kwargs):
|
||||||
"""
|
"""
|
||||||
:param env:
|
:param env: an class:`Environment` instance
|
||||||
an class:`Environment` instance
|
:param kwargs: additional keyword argument that some
|
||||||
:param kwargs:
|
processor might require.
|
||||||
additional keyword argument that some processor might require.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.env = env
|
self.env = env
|
||||||
@ -346,8 +358,7 @@ class BaseProcessor(object):
|
|||||||
def process_headers(self, headers):
|
def process_headers(self, headers):
|
||||||
"""Return processed `headers`
|
"""Return processed `headers`
|
||||||
|
|
||||||
:param headers:
|
:param headers: The headers as text.
|
||||||
The headers as text.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return headers
|
return headers
|
||||||
@ -355,14 +366,9 @@ class BaseProcessor(object):
|
|||||||
def process_body(self, content, content_type, subtype):
|
def process_body(self, content, content_type, subtype):
|
||||||
"""Return processed `content`.
|
"""Return processed `content`.
|
||||||
|
|
||||||
:param content:
|
:param content: The body content as text
|
||||||
The body content as text
|
:param content_type: Full content type, e.g., 'application/atom+xml'.
|
||||||
|
:param subtype: E.g. 'xml'.
|
||||||
:param content_type:
|
|
||||||
Full content type, e.g., 'application/atom+xml'.
|
|
||||||
|
|
||||||
:param subtype:
|
|
||||||
E.g. 'xml'.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return content
|
return content
|
||||||
@ -402,7 +408,8 @@ class PygmentsProcessor(BaseProcessor):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
style = get_style_by_name(self.kwargs['pygments_style'])
|
style = get_style_by_name(
|
||||||
|
self.kwargs.get('pygments_style', DEFAULT_STYLE))
|
||||||
except ClassNotFound:
|
except ClassNotFound:
|
||||||
style = Solarized256Style
|
style = Solarized256Style
|
||||||
|
|
||||||
@ -440,24 +447,35 @@ class HeadersProcessor(BaseProcessor):
|
|||||||
def process_headers(self, headers):
|
def process_headers(self, headers):
|
||||||
lines = headers.splitlines()
|
lines = headers.splitlines()
|
||||||
headers = sorted(lines[1:], key=lambda h: h.split(':')[0])
|
headers = sorted(lines[1:], key=lambda h: h.split(':')[0])
|
||||||
return '\n'.join(lines[:1] + headers)
|
return '\r\n'.join(lines[:1] + headers)
|
||||||
|
|
||||||
|
|
||||||
class OutputProcessor(object):
|
class OutputProcessor(object):
|
||||||
"""A delegate class that invokes the actual processors."""
|
"""A delegate class that invokes the actual processors."""
|
||||||
|
|
||||||
installed_processors = [
|
installed_processors = {
|
||||||
JSONProcessor,
|
'format': [
|
||||||
HeadersProcessor,
|
HeadersProcessor,
|
||||||
PygmentsProcessor
|
JSONProcessor
|
||||||
]
|
],
|
||||||
|
'colors': [
|
||||||
def __init__(self, env, **kwargs):
|
PygmentsProcessor
|
||||||
processors = [
|
|
||||||
cls(env, **kwargs)
|
|
||||||
for cls in self.installed_processors
|
|
||||||
]
|
]
|
||||||
self.processors = [p for p in processors if p.enabled]
|
}
|
||||||
|
|
||||||
|
def __init__(self, groups, env=Environment(), **kwargs):
|
||||||
|
"""
|
||||||
|
:param env: a :class:`models.Environment` instance
|
||||||
|
:param groups: the groups of processors to be applied
|
||||||
|
:param kwargs: additional keyword arguments for processors
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.processors = []
|
||||||
|
for group in groups:
|
||||||
|
for cls in self.installed_processors[group]:
|
||||||
|
processor = cls(env, **kwargs)
|
||||||
|
if processor.enabled:
|
||||||
|
self.processors.append(processor)
|
||||||
|
|
||||||
def process_headers(self, headers):
|
def process_headers(self, headers):
|
||||||
for processor in self.processors:
|
for processor in self.processors:
|
||||||
|
179
httpie/sessions.py
Normal file
179
httpie/sessions.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
"""Persistent, JSON-serialized sessions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import errno
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.cookies import RequestsCookieJar, create_cookie
|
||||||
|
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||||
|
|
||||||
|
from .compat import urlsplit
|
||||||
|
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||||
|
|
||||||
|
|
||||||
|
SESSIONS_DIR_NAME = 'sessions'
|
||||||
|
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def get_response(name, request_kwargs, config_dir, read_only=False):
|
||||||
|
"""Like `client.get_response`, but applies permanent
|
||||||
|
aspects of the session to the request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
|
||||||
|
host = Host(
|
||||||
|
root_dir=sessions_dir,
|
||||||
|
name=request_kwargs['headers'].get('Host', None)
|
||||||
|
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
|
||||||
|
)
|
||||||
|
session = Session(host, name)
|
||||||
|
session.load()
|
||||||
|
|
||||||
|
# Update session headers with the request headers.
|
||||||
|
session['headers'].update(request_kwargs.get('headers', {}))
|
||||||
|
# Use the merged headers for the request
|
||||||
|
request_kwargs['headers'] = session['headers']
|
||||||
|
|
||||||
|
auth = request_kwargs.get('auth', None)
|
||||||
|
if auth:
|
||||||
|
session.auth = auth
|
||||||
|
elif session.auth:
|
||||||
|
request_kwargs['auth'] = session.auth
|
||||||
|
|
||||||
|
requests_session = requests.Session()
|
||||||
|
requests_session.cookies = session.cookies
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests_session.request(**request_kwargs)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Existing sessions with `read_only=True` don't get updated.
|
||||||
|
if session.is_new or not read_only:
|
||||||
|
session.cookies = requests_session.cookies
|
||||||
|
session.save()
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class Host(object):
|
||||||
|
"""A host is a per-host directory on the disk containing sessions files."""
|
||||||
|
|
||||||
|
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.:-]+$')
|
||||||
|
|
||||||
|
def __init__(self, name, root_dir=DEFAULT_SESSIONS_DIR):
|
||||||
|
assert self.VALID_NAME_PATTERN.match(name)
|
||||||
|
self.name = name
|
||||||
|
self.root_dir = root_dir
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Return an iterator yielding `Session` instances."""
|
||||||
|
for fn in sorted(glob.glob1(self.path, '*.json')):
|
||||||
|
session_name = os.path.splitext(fn)[0]
|
||||||
|
yield Session(host=self, name=session_name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _quote_name(name):
|
||||||
|
"""host:port => host_port"""
|
||||||
|
return name.replace(':', '_')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unquote_name(name):
|
||||||
|
"""host_port => host:port"""
|
||||||
|
return re.sub(r'_(\d+)$', r':\1', name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls, root_dir=DEFAULT_SESSIONS_DIR):
|
||||||
|
"""Return a generator yielding a host at a time."""
|
||||||
|
for name in sorted(glob.glob1(root_dir, '*')):
|
||||||
|
if os.path.isdir(os.path.join(root_dir, name)):
|
||||||
|
yield Host(cls._unquote_name(name), root_dir=root_dir)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return '%s %s' % (self.name, self.path)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
shutil.rmtree(self.path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
path = os.path.join(self.root_dir, self._quote_name(self.name))
|
||||||
|
try:
|
||||||
|
os.makedirs(path, mode=0o700)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class Session(BaseConfigDict):
|
||||||
|
|
||||||
|
help = 'https://github.com/jkbr/httpie#sessions'
|
||||||
|
about = 'HTTPie session file'
|
||||||
|
|
||||||
|
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
||||||
|
|
||||||
|
def __init__(self, host, name, *args, **kwargs):
|
||||||
|
assert self.VALID_NAME_PATTERN.match(name)
|
||||||
|
super(Session, self).__init__(*args, **kwargs)
|
||||||
|
self.host = host
|
||||||
|
self.name = name
|
||||||
|
self['headers'] = {}
|
||||||
|
self['cookies'] = {}
|
||||||
|
self['auth'] = {
|
||||||
|
'type': None,
|
||||||
|
'username': None,
|
||||||
|
'password': None
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def directory(self):
|
||||||
|
return self.host.path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verbose_name(self):
|
||||||
|
return '%s %s %s' % (self.host.name, self.name, self.path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookies(self):
|
||||||
|
jar = RequestsCookieJar()
|
||||||
|
for name, cookie_dict in self['cookies'].items():
|
||||||
|
jar.set_cookie(create_cookie(
|
||||||
|
name, cookie_dict.pop('value'), **cookie_dict))
|
||||||
|
jar.clear_expired_cookies()
|
||||||
|
return jar
|
||||||
|
|
||||||
|
@cookies.setter
|
||||||
|
def cookies(self, jar):
|
||||||
|
# http://docs.python.org/2/library/cookielib.html#cookie-objects
|
||||||
|
stored_attrs = ['value', 'path', 'secure', 'expires']
|
||||||
|
self['cookies'] = {}
|
||||||
|
for host in jar._cookies.values():
|
||||||
|
for path in host.values():
|
||||||
|
for name, cookie in path.items():
|
||||||
|
self['cookies'][name] = dict(
|
||||||
|
(attname, getattr(cookie, attname))
|
||||||
|
for attname in stored_attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth(self):
|
||||||
|
auth = self.get('auth', None)
|
||||||
|
if not auth or not auth['type']:
|
||||||
|
return
|
||||||
|
Auth = {'basic': HTTPBasicAuth,
|
||||||
|
'digest': HTTPDigestAuth}[auth['type']]
|
||||||
|
return Auth(auth['username'], auth['password'])
|
||||||
|
|
||||||
|
@auth.setter
|
||||||
|
def auth(self, cred):
|
||||||
|
self['auth'] = {
|
||||||
|
'type': {HTTPBasicAuth: 'basic',
|
||||||
|
HTTPDigestAuth: 'digest'}[type(cred)],
|
||||||
|
'username': cred.username,
|
||||||
|
'password': cred.password,
|
||||||
|
}
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
#
|
17
setup.py
17
setup.py
@ -1,5 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
import codecs
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
import httpie
|
import httpie
|
||||||
|
|
||||||
@ -10,8 +12,7 @@ if sys.argv[-1] == 'test':
|
|||||||
|
|
||||||
|
|
||||||
requirements = [
|
requirements = [
|
||||||
# Debian has only requests==0.10.1 and httpie.deb depends on that.
|
'requests>=1.0.4',
|
||||||
'requests>=0.10.1',
|
|
||||||
'Pygments>=1.5'
|
'Pygments>=1.5'
|
||||||
]
|
]
|
||||||
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
if sys.version_info[:2] in ((2, 6), (3, 1)):
|
||||||
@ -22,11 +23,20 @@ if 'win32' in str(sys.platform).lower():
|
|||||||
requirements.append('colorama>=0.2.4')
|
requirements.append('colorama>=0.2.4')
|
||||||
|
|
||||||
|
|
||||||
|
def long_description():
|
||||||
|
"""Pre-process the README so that PyPi can render it properly."""
|
||||||
|
with codecs.open('README.rst', encoding='utf8') as f:
|
||||||
|
rst = f.read()
|
||||||
|
code_block = '(:\n\n)?\.\. code-block::.*'
|
||||||
|
rst = re.sub(code_block, '::', rst)
|
||||||
|
return rst
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='httpie',
|
name='httpie',
|
||||||
version=httpie.__version__,
|
version=httpie.__version__,
|
||||||
description=httpie.__doc__.strip(),
|
description=httpie.__doc__.strip(),
|
||||||
long_description=open('README.rst').read(),
|
long_description=long_description(),
|
||||||
url='http://httpie.org/',
|
url='http://httpie.org/',
|
||||||
download_url='https://github.com/jkbr/httpie',
|
download_url='https://github.com/jkbr/httpie',
|
||||||
author=httpie.__author__,
|
author=httpie.__author__,
|
||||||
@ -36,6 +46,7 @@ setup(
|
|||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'http = httpie.__main__:main',
|
'http = httpie.__main__:main',
|
||||||
|
'httpie = httpie.manage:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
|
397
tests/tests.py
397
tests/tests.py
@ -19,19 +19,23 @@ To make it run faster and offline you can::
|
|||||||
HTTPBIN_URL=http://localhost:5000 tox
|
HTTPBIN_URL=http://localhost:5000 tox
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import argparse
|
import argparse
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import shutil
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib2 import urlopen
|
from urllib2 import urlopen
|
||||||
try:
|
try:
|
||||||
from unittest import skipIf
|
from unittest import skipIf, skip
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
skip = lambda msg: lambda self: None
|
||||||
|
|
||||||
def skipIf(cond, reason):
|
def skipIf(cond, reason):
|
||||||
def decorator(test_method):
|
def decorator(test_method):
|
||||||
@ -40,9 +44,7 @@ except ImportError:
|
|||||||
return test_method
|
return test_method
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
from requests import __version__ as requests_version
|
||||||
from requests.compat import is_windows, is_py26, bytes, str
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
@ -53,14 +55,16 @@ from requests.compat import is_windows, is_py26, bytes, str
|
|||||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
|
||||||
|
|
||||||
from httpie import EXIT
|
from httpie import ExitStatus
|
||||||
from httpie import input
|
from httpie import input
|
||||||
from httpie.models import Environment
|
from httpie.models import Environment
|
||||||
from httpie.core import main
|
from httpie.core import main
|
||||||
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
||||||
from httpie.input import ParseError
|
from httpie.input import ParseError
|
||||||
|
from httpie.compat import is_windows, is_py26, bytes, str
|
||||||
|
|
||||||
|
|
||||||
|
CRLF = '\r\n'
|
||||||
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
||||||
'http://httpbin.org').rstrip('/')
|
'http://httpbin.org').rstrip('/')
|
||||||
|
|
||||||
@ -97,11 +101,16 @@ def httpbin(path):
|
|||||||
return HTTPBIN_URL + path
|
return HTTPBIN_URL + path
|
||||||
|
|
||||||
|
|
||||||
|
def mk_config_dir():
|
||||||
|
return tempfile.mkdtemp(prefix='httpie_test_config_dir_')
|
||||||
|
|
||||||
|
|
||||||
class TestEnvironment(Environment):
|
class TestEnvironment(Environment):
|
||||||
colors = 0
|
colors = 0
|
||||||
stdin_isatty = True,
|
stdin_isatty = True,
|
||||||
stdout_isatty = True
|
stdout_isatty = True
|
||||||
is_windows = False
|
is_windows = False
|
||||||
|
_shutil = shutil # we need it in __del__ (would get gc'd)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
|
||||||
@ -111,11 +120,43 @@ class TestEnvironment(Environment):
|
|||||||
if 'stderr' not in kwargs:
|
if 'stderr' not in kwargs:
|
||||||
kwargs['stderr'] = tempfile.TemporaryFile('w+t')
|
kwargs['stderr'] = tempfile.TemporaryFile('w+t')
|
||||||
|
|
||||||
|
self.delete_config_dir = False
|
||||||
|
if 'config_dir' not in kwargs:
|
||||||
|
kwargs['config_dir'] = mk_config_dir()
|
||||||
|
self.delete_config_dir = True
|
||||||
|
|
||||||
super(TestEnvironment, self).__init__(**kwargs)
|
super(TestEnvironment, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.delete_config_dir:
|
||||||
|
self._shutil.rmtree(self.config_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def has_docutils():
|
||||||
|
try:
|
||||||
|
#noinspection PyUnresolvedReferences
|
||||||
|
import docutils
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_readme_errors():
|
||||||
|
p = subprocess.Popen([
|
||||||
|
'rst2pseudoxml.py',
|
||||||
|
'--report=1',
|
||||||
|
'--exit-status=1',
|
||||||
|
os.path.join(TESTS_ROOT, '..', 'README.rst')
|
||||||
|
], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
err = p.communicate()[1]
|
||||||
|
if p.returncode:
|
||||||
|
return err
|
||||||
|
|
||||||
|
|
||||||
class BytesResponse(bytes):
|
class BytesResponse(bytes):
|
||||||
stderr = json = exit_status = None
|
stderr = json = exit_status = None
|
||||||
|
|
||||||
|
|
||||||
class StrResponse(str):
|
class StrResponse(str):
|
||||||
stderr = json = exit_status = None
|
stderr = json = exit_status = None
|
||||||
|
|
||||||
@ -142,18 +183,17 @@ def http(*args, **kwargs):
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exit_status = main(args=['--debug'] + list(args), **kwargs)
|
exit_status = main(args=['--traceback'] + list(args), **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
sys.stderr.write(env.stderr.read())
|
sys.stderr.write(env.stderr.read())
|
||||||
raise
|
raise
|
||||||
except SystemExit:
|
except SystemExit:
|
||||||
exit_status = EXIT.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
|
|
||||||
env.stdout.seek(0)
|
env.stdout.seek(0)
|
||||||
env.stderr.seek(0)
|
env.stderr.seek(0)
|
||||||
|
|
||||||
output = env.stdout.read()
|
output = env.stdout.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = StrResponse(output.decode('utf8'))
|
r = StrResponse(output.decode('utf8'))
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
@ -166,7 +206,7 @@ def http(*args, **kwargs):
|
|||||||
r.json = json.loads(r)
|
r.json = json.loads(r)
|
||||||
elif r.count('Content-Type:') == 1 and 'application/json' in r:
|
elif r.count('Content-Type:') == 1 and 'application/json' in r:
|
||||||
try:
|
try:
|
||||||
j = r.strip()[r.strip().rindex('\n\n'):]
|
j = r.strip()[r.strip().rindex('\r\n\r\n'):]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@ -187,6 +227,8 @@ def http(*args, **kwargs):
|
|||||||
|
|
||||||
class BaseTestCase(unittest.TestCase):
|
class BaseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
maxDiff = 100000
|
||||||
|
|
||||||
if is_py26:
|
if is_py26:
|
||||||
def assertIn(self, member, container, msg=None):
|
def assertIn(self, member, container, msg=None):
|
||||||
self.assertTrue(member in container, msg)
|
self.assertTrue(member in container, msg)
|
||||||
@ -502,8 +544,8 @@ class ImplicitHTTPMethodTest(BaseTestCase):
|
|||||||
self.assertIn(OK, r)
|
self.assertIn(OK, r)
|
||||||
|
|
||||||
|
|
||||||
class PrettyFlagTest(BaseTestCase):
|
class PrettyOptionsTest(BaseTestCase):
|
||||||
"""Test the --pretty / --ugly flag handling."""
|
"""Test the --pretty flag handling."""
|
||||||
|
|
||||||
def test_pretty_enabled_by_default(self):
|
def test_pretty_enabled_by_default(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -522,7 +564,7 @@ class PrettyFlagTest(BaseTestCase):
|
|||||||
|
|
||||||
def test_force_pretty(self):
|
def test_force_pretty(self):
|
||||||
r = http(
|
r = http(
|
||||||
'--pretty',
|
'--pretty=all',
|
||||||
'GET',
|
'GET',
|
||||||
httpbin('/get'),
|
httpbin('/get'),
|
||||||
env=TestEnvironment(stdout_isatty=False, colors=256),
|
env=TestEnvironment(stdout_isatty=False, colors=256),
|
||||||
@ -531,7 +573,7 @@ class PrettyFlagTest(BaseTestCase):
|
|||||||
|
|
||||||
def test_force_ugly(self):
|
def test_force_ugly(self):
|
||||||
r = http(
|
r = http(
|
||||||
'--ugly',
|
'--pretty=none',
|
||||||
'GET',
|
'GET',
|
||||||
httpbin('/get'),
|
httpbin('/get'),
|
||||||
)
|
)
|
||||||
@ -544,7 +586,7 @@ class PrettyFlagTest(BaseTestCase):
|
|||||||
"""
|
"""
|
||||||
r = http(
|
r = http(
|
||||||
'--print=B',
|
'--print=B',
|
||||||
'--pretty',
|
'--pretty=all',
|
||||||
httpbin('/post'),
|
httpbin('/post'),
|
||||||
'Content-Type:text/foo+json',
|
'Content-Type:text/foo+json',
|
||||||
'a=b',
|
'a=b',
|
||||||
@ -552,6 +594,34 @@ class PrettyFlagTest(BaseTestCase):
|
|||||||
)
|
)
|
||||||
self.assertIn(COLOR, r)
|
self.assertIn(COLOR, r)
|
||||||
|
|
||||||
|
def test_colors_option(self):
|
||||||
|
r = http(
|
||||||
|
'--print=B',
|
||||||
|
'--pretty=colors',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
'a=b',
|
||||||
|
env=TestEnvironment(colors=256),
|
||||||
|
)
|
||||||
|
#noinspection PyUnresolvedReferences
|
||||||
|
# Tests that the JSON data isn't formatted.
|
||||||
|
self.assertEqual(r.strip().count('\n'), 0)
|
||||||
|
self.assertIn(COLOR, r)
|
||||||
|
|
||||||
|
def test_format_option(self):
|
||||||
|
r = http(
|
||||||
|
'--print=B',
|
||||||
|
'--pretty=format',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
'a=b',
|
||||||
|
env=TestEnvironment(colors=256),
|
||||||
|
)
|
||||||
|
#noinspection PyUnresolvedReferences
|
||||||
|
# Tests that the JSON data is formatted.
|
||||||
|
self.assertEqual(r.strip().count('\n'), 2)
|
||||||
|
self.assertNotIn(COLOR, r)
|
||||||
|
|
||||||
|
|
||||||
class VerboseFlagTest(BaseTestCase):
|
class VerboseFlagTest(BaseTestCase):
|
||||||
|
|
||||||
@ -688,7 +758,7 @@ class BinaryResponseDataTest(BaseTestCase):
|
|||||||
|
|
||||||
def test_binary_suppresses_when_not_terminal_but_pretty(self):
|
def test_binary_suppresses_when_not_terminal_but_pretty(self):
|
||||||
r = http(
|
r = http(
|
||||||
'--pretty',
|
'--pretty=all',
|
||||||
'GET',
|
'GET',
|
||||||
self.url,
|
self.url,
|
||||||
env=TestEnvironment(stdin_isatty=True,
|
env=TestEnvironment(stdin_isatty=True,
|
||||||
@ -765,6 +835,8 @@ class AuthTest(BaseTestCase):
|
|||||||
self.assertIn('"authenticated": true', r)
|
self.assertIn('"authenticated": true', r)
|
||||||
self.assertIn('"user": "user"', r)
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
|
@skipIf(requests_version == '0.13.6',
|
||||||
|
'Redirects with prefetch=False are broken in Requests 0.13.6')
|
||||||
def test_digest_auth(self):
|
def test_digest_auth(self):
|
||||||
r = http(
|
r = http(
|
||||||
'--auth-type=digest',
|
'--auth-type=digest',
|
||||||
@ -791,6 +863,31 @@ class AuthTest(BaseTestCase):
|
|||||||
self.assertIn('"authenticated": true', r)
|
self.assertIn('"authenticated": true', r)
|
||||||
self.assertIn('"user": "user"', r)
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
|
def test_credentials_in_url(self):
|
||||||
|
url = httpbin('/basic-auth/user/password')
|
||||||
|
url = 'http://user:password@' + url.split('http://', 1)[1]
|
||||||
|
r = http(
|
||||||
|
'GET',
|
||||||
|
url
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r)
|
||||||
|
self.assertIn('"authenticated": true', r)
|
||||||
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
|
def test_credentials_in_url_auth_flag_has_priority(self):
|
||||||
|
"""When credentials are passed in URL and via -a at the same time,
|
||||||
|
then the ones from -a are used."""
|
||||||
|
url = httpbin('/basic-auth/user/password')
|
||||||
|
url = 'http://user:wrong_password@' + url.split('http://', 1)[1]
|
||||||
|
r = http(
|
||||||
|
'--auth=user:password',
|
||||||
|
'GET',
|
||||||
|
url
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r)
|
||||||
|
self.assertIn('"authenticated": true', r)
|
||||||
|
self.assertIn('"user": "user"', r)
|
||||||
|
|
||||||
|
|
||||||
class ExitStatusTest(BaseTestCase):
|
class ExitStatusTest(BaseTestCase):
|
||||||
|
|
||||||
@ -800,7 +897,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/200')
|
httpbin('/status/200')
|
||||||
)
|
)
|
||||||
self.assertIn(OK, r)
|
self.assertIn(OK, r)
|
||||||
self.assertEqual(r.exit_status, EXIT.OK)
|
self.assertEqual(r.exit_status, ExitStatus.OK)
|
||||||
|
|
||||||
def test_error_response_exits_0_without_check_status(self):
|
def test_error_response_exits_0_without_check_status(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -808,7 +905,16 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/500')
|
httpbin('/status/500')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 500', r)
|
self.assertIn('HTTP/1.1 500', r)
|
||||||
self.assertEqual(r.exit_status, EXIT.OK)
|
self.assertEqual(r.exit_status, ExitStatus.OK)
|
||||||
|
self.assertTrue(not r.stderr)
|
||||||
|
|
||||||
|
def test_timeout_exit_status(self):
|
||||||
|
r = http(
|
||||||
|
'--timeout=0.5',
|
||||||
|
'GET',
|
||||||
|
httpbin('/delay/1')
|
||||||
|
)
|
||||||
|
self.assertEqual(r.exit_status, ExitStatus.ERROR_TIMEOUT)
|
||||||
|
|
||||||
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -819,19 +925,21 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
env=TestEnvironment(stdout_isatty=False,)
|
env=TestEnvironment(stdout_isatty=False,)
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 301', r)
|
self.assertIn('HTTP/1.1 301', r)
|
||||||
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_3XX)
|
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_3XX)
|
||||||
self.assertIn('301 moved permanently', r.stderr.lower())
|
self.assertIn('301 moved permanently', r.stderr.lower())
|
||||||
|
|
||||||
|
@skipIf(requests_version == '0.13.6',
|
||||||
|
'Redirects with prefetch=False are broken in Requests 0.13.6')
|
||||||
def test_3xx_check_status_redirects_allowed_exits_0(self):
|
def test_3xx_check_status_redirects_allowed_exits_0(self):
|
||||||
r = http(
|
r = http(
|
||||||
'--check-status',
|
'--check-status',
|
||||||
'--allow-redirects',
|
'--follow',
|
||||||
'GET',
|
'GET',
|
||||||
httpbin('/status/301')
|
httpbin('/status/301')
|
||||||
)
|
)
|
||||||
# The redirect will be followed so 200 is expected.
|
# The redirect will be followed so 200 is expected.
|
||||||
self.assertIn('HTTP/1.1 200 OK', r)
|
self.assertIn('HTTP/1.1 200 OK', r)
|
||||||
self.assertEqual(r.exit_status, EXIT.OK)
|
self.assertEqual(r.exit_status, ExitStatus.OK)
|
||||||
|
|
||||||
def test_4xx_check_status_exits_4(self):
|
def test_4xx_check_status_exits_4(self):
|
||||||
r = http(
|
r = http(
|
||||||
@ -840,7 +948,7 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/401')
|
httpbin('/status/401')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 401', r)
|
self.assertIn('HTTP/1.1 401', r)
|
||||||
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_4XX)
|
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_4XX)
|
||||||
# Also stderr should be empty since stdout isn't redirected.
|
# Also stderr should be empty since stdout isn't redirected.
|
||||||
self.assertTrue(not r.stderr)
|
self.assertTrue(not r.stderr)
|
||||||
|
|
||||||
@ -851,34 +959,32 @@ class ExitStatusTest(BaseTestCase):
|
|||||||
httpbin('/status/500')
|
httpbin('/status/500')
|
||||||
)
|
)
|
||||||
self.assertIn('HTTP/1.1 500', r)
|
self.assertIn('HTTP/1.1 500', r)
|
||||||
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_5XX)
|
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_5XX)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsOnlyTests(BaseTestCase):
|
||||||
|
|
||||||
|
@skip('FIXME: kills the runner')
|
||||||
|
#@skipIf(not is_windows, 'windows-only')
|
||||||
|
def test_windows_colorized_output(self):
|
||||||
|
# Spits out the colorized output.
|
||||||
|
http(httpbin('/get'), env=Environment())
|
||||||
|
|
||||||
|
|
||||||
class FakeWindowsTest(BaseTestCase):
|
class FakeWindowsTest(BaseTestCase):
|
||||||
|
|
||||||
def test_stdout_redirect_not_supported_on_windows(self):
|
|
||||||
env = TestEnvironment(is_windows=True, stdout_isatty=False)
|
|
||||||
r = http(
|
|
||||||
'GET',
|
|
||||||
httpbin('/get'),
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
self.assertNotEqual(r.exit_status, EXIT.OK)
|
|
||||||
self.assertIn('Windows', r.stderr)
|
|
||||||
self.assertIn('--output', r.stderr)
|
|
||||||
|
|
||||||
def test_output_file_pretty_not_allowed_on_windows(self):
|
def test_output_file_pretty_not_allowed_on_windows(self):
|
||||||
|
|
||||||
r = http(
|
r = http(
|
||||||
'--output',
|
'--output',
|
||||||
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
|
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
|
||||||
'--pretty',
|
'--pretty=all',
|
||||||
'GET',
|
'GET',
|
||||||
httpbin('/get'),
|
httpbin('/get'),
|
||||||
env=TestEnvironment(is_windows=True)
|
env=TestEnvironment(is_windows=True)
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'Only terminal output can be prettified on Windows', r.stderr)
|
'Only terminal output can be colorized on Windows', r.stderr)
|
||||||
|
|
||||||
|
|
||||||
class StreamTest(BaseTestCase):
|
class StreamTest(BaseTestCase):
|
||||||
@ -890,7 +996,7 @@ class StreamTest(BaseTestCase):
|
|||||||
with open(BIN_FILE_PATH, 'rb') as f:
|
with open(BIN_FILE_PATH, 'rb') as f:
|
||||||
r = http(
|
r = http(
|
||||||
'--verbose',
|
'--verbose',
|
||||||
'--pretty',
|
'--pretty=all',
|
||||||
'--stream',
|
'--stream',
|
||||||
'GET',
|
'GET',
|
||||||
httpbin('/get'),
|
httpbin('/get'),
|
||||||
@ -906,10 +1012,11 @@ class StreamTest(BaseTestCase):
|
|||||||
#self.assertIn(OK_COLOR, r)
|
#self.assertIn(OK_COLOR, r)
|
||||||
|
|
||||||
def test_encoded_stream(self):
|
def test_encoded_stream(self):
|
||||||
"""Test that --stream works with non-prettified redirected terminal output."""
|
"""Test that --stream works with non-prettified
|
||||||
|
redirected terminal output."""
|
||||||
with open(BIN_FILE_PATH, 'rb') as f:
|
with open(BIN_FILE_PATH, 'rb') as f:
|
||||||
r = http(
|
r = http(
|
||||||
'--ugly',
|
'--pretty=none',
|
||||||
'--stream',
|
'--stream',
|
||||||
'--verbose',
|
'--verbose',
|
||||||
'GET',
|
'GET',
|
||||||
@ -924,10 +1031,11 @@ class StreamTest(BaseTestCase):
|
|||||||
#self.assertIn(OK, r)
|
#self.assertIn(OK, r)
|
||||||
|
|
||||||
def test_redirected_stream(self):
|
def test_redirected_stream(self):
|
||||||
"""Test that --stream works with non-prettified redirected terminal output."""
|
"""Test that --stream works with non-prettified
|
||||||
|
redirected terminal output."""
|
||||||
with open(BIN_FILE_PATH, 'rb') as f:
|
with open(BIN_FILE_PATH, 'rb') as f:
|
||||||
r = http(
|
r = http(
|
||||||
'--ugly',
|
'--pretty=none',
|
||||||
'--stream',
|
'--stream',
|
||||||
'--verbose',
|
'--verbose',
|
||||||
'GET',
|
'GET',
|
||||||
@ -943,6 +1051,67 @@ class StreamTest(BaseTestCase):
|
|||||||
self.assertIn(BIN_FILE_CONTENT, r)
|
self.assertIn(BIN_FILE_CONTENT, r)
|
||||||
|
|
||||||
|
|
||||||
|
class LineEndingsTest(BaseTestCase):
|
||||||
|
"""Test that CRLF is properly used in headers and
|
||||||
|
as the headers/body separator."""
|
||||||
|
|
||||||
|
def _validate_crlf(self, msg):
|
||||||
|
lines = iter(msg.splitlines(True))
|
||||||
|
for header in lines:
|
||||||
|
if header == CRLF:
|
||||||
|
break
|
||||||
|
self.assertTrue(header.endswith(CRLF), repr(header))
|
||||||
|
else:
|
||||||
|
self.fail('CRLF between headers and body not found in %r' % msg)
|
||||||
|
body = ''.join(lines)
|
||||||
|
self.assertNotIn(CRLF, body)
|
||||||
|
return body
|
||||||
|
|
||||||
|
def test_CRLF_headers_only(self):
|
||||||
|
r = http(
|
||||||
|
'--headers',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
body = self._validate_crlf(r)
|
||||||
|
self.assertFalse(body, 'Garbage after headers: %r' % r)
|
||||||
|
|
||||||
|
def test_CRLF_ugly_response(self):
|
||||||
|
r = http(
|
||||||
|
'--pretty=none',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self._validate_crlf(r)
|
||||||
|
|
||||||
|
def test_CRLF_formatted_response(self):
|
||||||
|
r = http(
|
||||||
|
'--pretty=format',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self.assertEqual(r.exit_status, 0)
|
||||||
|
self._validate_crlf(r)
|
||||||
|
|
||||||
|
def test_CRLF_ugly_request(self):
|
||||||
|
r = http(
|
||||||
|
'--pretty=none',
|
||||||
|
'--print=HB',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self._validate_crlf(r)
|
||||||
|
|
||||||
|
def test_CRLF_formatted_request(self):
|
||||||
|
r = http(
|
||||||
|
'--pretty=format',
|
||||||
|
'--print=HB',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self._validate_crlf(r)
|
||||||
|
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
# CLI argument parsing related tests.
|
# CLI argument parsing related tests.
|
||||||
#################################################################
|
#################################################################
|
||||||
@ -1093,11 +1262,153 @@ class ArgumentParserTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(args.items, [
|
self.assertEqual(args.items, [
|
||||||
input.KeyValue(
|
input.KeyValue(
|
||||||
key='new_item', value='a', sep='=', orig='new_item=a'),
|
key='new_item', value='a', sep='=', orig='new_item=a'),
|
||||||
input.KeyValue(key
|
input.KeyValue(
|
||||||
='old_item', value='b', sep='=', orig='old_item=b'),
|
key='old_item', value='b', sep='=', orig='old_item=b'),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoOptions(BaseTestCase):
|
||||||
|
|
||||||
|
def test_valid_no_options(self):
|
||||||
|
r = http(
|
||||||
|
'--verbose',
|
||||||
|
'--no-verbose',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self.assertNotIn('GET /get HTTP/1.1', r)
|
||||||
|
|
||||||
|
def test_invalid_no_options(self):
|
||||||
|
r = http(
|
||||||
|
'--no-war',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get')
|
||||||
|
)
|
||||||
|
self.assertEqual(r.exit_status, 1)
|
||||||
|
self.assertIn('unrecognized arguments: --no-war', r.stderr)
|
||||||
|
self.assertNotIn('GET /get HTTP/1.1', r)
|
||||||
|
|
||||||
|
|
||||||
|
class READMETest(BaseTestCase):
|
||||||
|
|
||||||
|
@skipIf(not has_docutils(), 'docutils not installed')
|
||||||
|
def test_README_reStructuredText_valid(self):
|
||||||
|
errors = get_readme_errors()
|
||||||
|
self.assertFalse(errors, msg=errors)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionTest(BaseTestCase):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def env(self):
|
||||||
|
return TestEnvironment(config_dir=self.config_dir)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Start a full-blown session with a custom request header,
|
||||||
|
# authorization, and response cookies.
|
||||||
|
self.config_dir = mk_config_dir()
|
||||||
|
r = http(
|
||||||
|
'--follow',
|
||||||
|
'--session=test',
|
||||||
|
'--auth=username:password',
|
||||||
|
'GET',
|
||||||
|
httpbin('/cookies/set?hello=world'),
|
||||||
|
'Hello:World',
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.config_dir)
|
||||||
|
|
||||||
|
def test_session_create(self):
|
||||||
|
# Verify that the session has been created.
|
||||||
|
r = http(
|
||||||
|
'--session=test',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r)
|
||||||
|
|
||||||
|
self.assertEqual(r.json['headers']['Hello'], 'World')
|
||||||
|
self.assertEqual(r.json['headers']['Cookie'], 'hello=world')
|
||||||
|
self.assertIn('Basic ', r.json['headers']['Authorization'])
|
||||||
|
|
||||||
|
def test_session_update(self):
|
||||||
|
# Get a response to a request from the original session.
|
||||||
|
r1 = http(
|
||||||
|
'--session=test',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r1)
|
||||||
|
|
||||||
|
# Make a request modifying the session data.
|
||||||
|
r2 = http(
|
||||||
|
'--follow',
|
||||||
|
'--session=test',
|
||||||
|
'--auth=username:password2',
|
||||||
|
'GET',
|
||||||
|
httpbin('/cookies/set?hello=world2'),
|
||||||
|
'Hello:World2',
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r2)
|
||||||
|
|
||||||
|
# Get a response to a request from the updated session.
|
||||||
|
r3 = http(
|
||||||
|
'--session=test',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r3)
|
||||||
|
|
||||||
|
self.assertEqual(r3.json['headers']['Hello'], 'World2')
|
||||||
|
self.assertEqual(r3.json['headers']['Cookie'], 'hello=world2')
|
||||||
|
self.assertNotEqual(r1.json['headers']['Authorization'],
|
||||||
|
r3.json['headers']['Authorization'])
|
||||||
|
|
||||||
|
def test_session_read_only(self):
|
||||||
|
# Get a response from the original session.
|
||||||
|
r1 = http(
|
||||||
|
'--session=test',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r1)
|
||||||
|
|
||||||
|
# Make a request modifying the session data but
|
||||||
|
# with --session-read-only.
|
||||||
|
r2 = http(
|
||||||
|
'--follow',
|
||||||
|
'--session-read-only=test',
|
||||||
|
'--auth=username:password2',
|
||||||
|
'GET',
|
||||||
|
httpbin('/cookies/set?hello=world2'),
|
||||||
|
'Hello:World2',
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r2)
|
||||||
|
|
||||||
|
# Get a response from the updated session.
|
||||||
|
r3 = http(
|
||||||
|
'--session=test',
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r3)
|
||||||
|
|
||||||
|
# Origin can differ on Travis.
|
||||||
|
del r1.json['origin'], r3.json['origin']
|
||||||
|
|
||||||
|
# Should be the same as before r2.
|
||||||
|
self.assertDictEqual(r1.json, r3.json)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
#noinspection PyCallingNonCallable
|
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Reference in New Issue
Block a user