mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 10:27:47 +02:00
Compare commits
92 Commits
Author | SHA1 | Date | |
---|---|---|---|
f0c42cd089 | |||
5c6cea79a1 | |||
2bed81059a | |||
be0b2f21d2 | |||
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
|
||||
.coverage
|
||||
htmlcov
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
|
@ -3,7 +3,7 @@ python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- pypy
|
||||
- 3.2
|
||||
- 3.3
|
||||
script: python setup.py test
|
||||
install:
|
||||
- pip install . --use-mirrors
|
||||
|
@ -8,6 +8,7 @@ HTTPie authors
|
||||
Patches and ideas
|
||||
-----------------
|
||||
|
||||
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
|
||||
* `Hank Gay <https://github.com/gthank>`_
|
||||
* `Jake Basile <https://github.com/jakebasile>`_
|
||||
* `Vladimir Berkutov <https://github.com/dair-targ>`_
|
||||
|
408
README.rst
408
README.rst
@ -1,12 +1,11 @@
|
||||
***********************
|
||||
HTTPie: cURL for Humans
|
||||
***********************
|
||||
****************************************
|
||||
HTTPie: a CLI, cURL-like tool for humans
|
||||
****************************************
|
||||
|
||||
v0.2.7
|
||||
|
||||
HTTPie is a **command line HTTP client** whose goal is to make CLI interaction
|
||||
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
|
||||
for **testing, debugging**, and generally **interacting** with HTTP servers.
|
||||
|
||||
@ -15,7 +14,11 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
|
||||
:alt: HTTPie compared to cURL
|
||||
:width: 835
|
||||
:height: 835
|
||||
:align: center
|
||||
|
||||
.. image:: https://raw.github.com/claudiatd/httpie-artwork/master/images/httpie_logo_simple.png
|
||||
:alt: HTTPie logo
|
||||
:align: center
|
||||
|
||||
HTTPie is written in Python, and under the hood it uses the excellent
|
||||
`Requests`_ and `Pygments`_ libraries.
|
||||
@ -30,7 +33,6 @@ HTTPie is written in Python, and under the hood it uses the excellent
|
||||
:backlinks: none
|
||||
|
||||
|
||||
|
||||
=============
|
||||
Main Features
|
||||
=============
|
||||
@ -39,10 +41,11 @@ Main Features
|
||||
* Formatted and colorized terminal output
|
||||
* Built-in JSON support
|
||||
* Forms and file uploads
|
||||
* HTTPS and authorization
|
||||
* HTTPS, proxies, and authentication
|
||||
* Arbitrary request data
|
||||
* 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
|
||||
* Documentation
|
||||
* Test coverage
|
||||
@ -58,9 +61,11 @@ or ``easy_install``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install -U httpie
|
||||
$ pip install --upgrade httpie
|
||||
|
||||
|
||||
Alternatively:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ easy_install httpie
|
||||
@ -76,16 +81,17 @@ Or, you can install the **development version** directly from GitHub:
|
||||
|
||||
.. 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
|
||||
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:
|
||||
@ -111,53 +117,64 @@ Examples
|
||||
--------
|
||||
|
||||
|
||||
Send a ``HEAD`` request:
|
||||
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
|
||||
|
||||
.. 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
|
||||
|
||||
$ 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
|
||||
|
||||
$ http PUT example.org X-API-Token:123 name='David Bowie'
|
||||
|
||||
See the request that is being sent:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http --verbose example.org
|
||||
$ http -v 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
|
||||
|
||||
$ 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
|
||||
|
||||
$ http example.org < file.json
|
||||
|
||||
|
||||
Download a file:
|
||||
Download a file and save it via `redirected output`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ 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
|
||||
============
|
||||
@ -169,7 +186,7 @@ The name of the HTTP method comes right before the URL argument:
|
||||
$ 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
|
||||
|
||||
@ -177,29 +194,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
|
||||
either ``GET`` or ``POST``. This depends on whether you are sending
|
||||
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
|
||||
either ``GET`` (with no request data) or ``POST`` (with request data).
|
||||
|
||||
|
||||
===========
|
||||
@ -266,11 +261,12 @@ their type is distinguished only by the separator used:
|
||||
+-----------------------+-----------------------------------------------------+
|
||||
|
||||
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.
|
||||
|
||||
No that data fields aren't the only way to specify request data,
|
||||
`redirected input`_ allows passing arbitrary data to be sent with the request.
|
||||
Note that data fields aren't the only way to specify request data:
|
||||
`Redirected input`_ allows for passing arbitrary data to be sent with the
|
||||
request.
|
||||
|
||||
|
||||
====
|
||||
@ -278,7 +274,7 @@ JSON
|
||||
====
|
||||
|
||||
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
|
||||
object by default. HTTPie also automatically sets the following headers,
|
||||
@ -289,9 +285,9 @@ both of which can be overwritten:
|
||||
``Accept`` ``application/json``
|
||||
================ =======================================
|
||||
|
||||
You can use ``--json`` / ``-j`` to set ``Accept`` to ``application/json``
|
||||
regardless of whether you are sending data (it's a shortcut for using setting
|
||||
the header via the usual header notation –
|
||||
You can use ``--json`` / ``-j`` to explicitly set ``Accept``
|
||||
to ``application/json`` regardless of whether you are sending data
|
||||
(it's a shortcut for setting the header via the usual header notation –
|
||||
``http url Accept:application/json``).
|
||||
|
||||
Simple example:
|
||||
@ -342,6 +338,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
|
||||
@ -349,9 +351,13 @@ Forms
|
||||
|
||||
Submitting forms is very similar to sending `JSON`_ requests. Often the only
|
||||
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``.
|
||||
|
||||
It is possible to make form data the implicit content type instead of JSON
|
||||
via the `config`_ file.
|
||||
|
||||
|
||||
-------------
|
||||
Regular Forms
|
||||
-------------
|
||||
@ -374,7 +380,7 @@ Regular 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``:
|
||||
|
||||
.. code-block:: bash
|
||||
@ -416,8 +422,7 @@ To set custom headers you can use the ``Header:Value`` notation:
|
||||
X-Foo: Bar
|
||||
|
||||
|
||||
There are a couple of default headers that HTTPie sets, but they can easily
|
||||
be overwritten:
|
||||
There are a couple of default headers that HTTPie sets:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
@ -428,12 +433,15 @@ be overwritten:
|
||||
Host: <taken-from-URL>
|
||||
|
||||
|
||||
====
|
||||
Auth
|
||||
====
|
||||
Any of the default headers can be overwritten.
|
||||
|
||||
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
|
||||
@ -441,12 +449,16 @@ come). There are two flags that control authorization:
|
||||
(``-a username``), you'll be prompted for
|
||||
the password before the request is sent.
|
||||
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
|
||||
``basic`` and ``digest``. The default value is
|
||||
``basic`` so it can often be omitted.
|
||||
=================== ======================================================
|
||||
|
||||
Authorization information from ``.netrc`` is honored as well.
|
||||
|
||||
Basic auth:
|
||||
|
||||
@ -471,6 +483,47 @@ With password prompt:
|
||||
$ 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
|
||||
==============
|
||||
@ -530,7 +583,7 @@ Character Stands for
|
||||
``b`` Response body.
|
||||
========== ==================
|
||||
|
||||
Print both the request and response headers:
|
||||
Print request and response headers:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -547,14 +600,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
|
||||
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
|
||||
|
||||
$ 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.
|
||||
Therefore, bandwidth and time isn't wasted downloading the body
|
||||
which you don't care about.
|
||||
@ -603,14 +656,14 @@ You can use ``cat`` to enter multiline data on the terminal:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cat | http POST example.com⏎
|
||||
$ cat | http POST example.com
|
||||
<paste>
|
||||
^D
|
||||
|
||||
|
||||
.. 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
|
||||
- call parents
|
||||
^D
|
||||
@ -635,7 +688,7 @@ Body Data From a Filename
|
||||
``@/path/to/file``) whose content is used as if it came from ``stdin``.
|
||||
|
||||
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
|
||||
verbatim contents of that XML file with ``Content-Type: application/xml``:
|
||||
|
||||
@ -648,7 +701,8 @@ verbatim contents of that XML file with ``Content-Type: application/xml``:
|
||||
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 +710,42 @@ Colors and Formatting
|
||||
---------------------
|
||||
|
||||
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.
|
||||
* JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||
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 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
|
||||
that the response body is binary,
|
||||
|
||||
.. 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
|
||||
|
||||
@ -701,7 +767,7 @@ Redirected Output
|
||||
HTTPie uses **different defaults** for redirected output than for
|
||||
`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).
|
||||
* Also, binary data isn't suppressed.
|
||||
|
||||
@ -723,12 +789,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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
.. 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 +817,7 @@ Streamed Responses
|
||||
|
||||
Responses are downloaded and printed in chunks, which allows for streaming
|
||||
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.
|
||||
|
||||
|
||||
@ -748,7 +828,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
|
||||
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.
|
||||
|
||||
|
||||
@ -759,7 +839,7 @@ Prettified streamed response:
|
||||
$ 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
|
||||
|
||||
@ -768,6 +848,94 @@ Streamed output by small chunks:
|
||||
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track=Apple \
|
||||
| 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
|
||||
@ -776,17 +944,19 @@ Scripting
|
||||
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
|
||||
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
|
||||
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``,
|
||||
respectively:
|
||||
be ``3`` (unless ``--follow`` is set), ``4``, or ``5``,
|
||||
respectively. Also, the ``--timeout`` option allows to overwrite the default
|
||||
30s timeout:
|
||||
|
||||
.. code-block:: 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!'
|
||||
else
|
||||
case $? in
|
||||
2) echo 'Request timed out!' ;;
|
||||
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
|
||||
4) echo 'HTTP 4xx Client Error!' ;;
|
||||
5) echo 'HTTP 5xx Server Error!' ;;
|
||||
@ -831,10 +1001,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``
|
||||
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
|
||||
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
|
||||
@ -869,7 +1044,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
|
||||
`supported Python environments`_:
|
||||
|
||||
.. code-b®lock:: bash
|
||||
.. code-block:: bash
|
||||
|
||||
# Install tox
|
||||
pip install tox
|
||||
@ -878,8 +1053,13 @@ Please run the existing suite of tests before a pull request is submitted:
|
||||
tox
|
||||
|
||||
|
||||
Don't forget to add yourself to `AUTHORS`_.
|
||||
Don't forget to add yourself to `AUTHORS.rst`_.
|
||||
|
||||
=======
|
||||
Logo
|
||||
=======
|
||||
|
||||
See `claudiatd/httpie-artwork`_
|
||||
|
||||
=======
|
||||
Authors
|
||||
@ -888,7 +1068,6 @@ Authors
|
||||
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
|
||||
have contributed.
|
||||
|
||||
|
||||
=======
|
||||
Licence
|
||||
=======
|
||||
@ -900,14 +1079,37 @@ Please see `LICENSE`_.
|
||||
Changelog
|
||||
=========
|
||||
|
||||
* `0.2.8dev`_
|
||||
*You can click a version name to see a diff with the previous one.*
|
||||
|
||||
* `0.4.1`_ (2013-02-26)
|
||||
* Fixed ``setup.py``.
|
||||
* `0.4.0`_ (2013-02-22)
|
||||
* 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)
|
||||
* Compatibility with Requests 0.13.6.
|
||||
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
|
||||
streaming also with ``--pretty`` and to ensure a more frequent output
|
||||
flushing.
|
||||
* 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``).
|
||||
* Improved content type matching.
|
||||
* Updated Solarized color scheme.
|
||||
@ -973,6 +1175,7 @@ Changelog
|
||||
.. _Jakub Roztocil: http://roztocil.name
|
||||
.. _@jakubroztocil: https://twitter.com/jakubroztocil
|
||||
.. _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.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
|
||||
@ -980,7 +1183,10 @@ Changelog
|
||||
.. _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.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
|
||||
.. _README for stable version: https://github.com/jkbr/httpie/tree/0.2.6#readme
|
||||
.. _AUTHORS: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
|
||||
.. _0.3.0: https://github.com/jkbr/httpie/compare/0.2.7...0.3.0
|
||||
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
|
||||
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
|
||||
.. _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
|
||||
|
@ -1,16 +1,19 @@
|
||||
"""
|
||||
HTTPie - cURL for humans.
|
||||
HTTPie - a CLI, cURL-like tool for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.2.7'
|
||||
__version__ = '0.4.1'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
class EXIT:
|
||||
class ExitStatus:
|
||||
"""Exit status code constants."""
|
||||
OK = 0
|
||||
ERROR = 1
|
||||
# Used only when requested:
|
||||
ERROR_TIMEOUT = 2
|
||||
|
||||
# Used only when requested with --check-status:
|
||||
ERROR_HTTP_3XX = 3
|
||||
ERROR_HTTP_4XX = 4
|
||||
ERROR_HTTP_5XX = 5
|
||||
|
364
httpie/cli.py
364
httpie/cli.py
@ -1,20 +1,22 @@
|
||||
"""CLI arguments definition.
|
||||
|
||||
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 requests.compat import is_windows
|
||||
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from .compat import is_windows
|
||||
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||
PRETTIFY_STDOUT_TTY_ONLY,
|
||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
|
||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS)
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
||||
|
||||
|
||||
def _(text):
|
||||
@ -22,15 +24,72 @@ def _(text):
|
||||
return ' '.join(text.strip().split())
|
||||
|
||||
|
||||
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
parser = Parser(
|
||||
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.
|
||||
#############################################
|
||||
###############################################################################
|
||||
|
||||
group_type = parser.add_mutually_exclusive_group(required=False)
|
||||
group_type.add_argument(
|
||||
content_type = parser.add_argument_group(
|
||||
title='Predefined content types',
|
||||
description=None
|
||||
)
|
||||
|
||||
content_type.add_argument(
|
||||
'--json', '-j', action='store_true',
|
||||
help=_('''
|
||||
(default) Data items from the command
|
||||
@ -39,55 +98,66 @@ group_type.add_argument(
|
||||
are set to application/json (if not specified).
|
||||
''')
|
||||
)
|
||||
group_type.add_argument(
|
||||
content_type.add_argument(
|
||||
'--form', '-f', action='store_true',
|
||||
help=_('''
|
||||
Data items from the command line are serialized as form fields.
|
||||
The Content-Type is set to application/x-www-form-urlencoded
|
||||
(if not specified).
|
||||
The presence of any file fields results
|
||||
into a multipart/form-data request.
|
||||
in a multipart/form-data request.
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
# Output options.
|
||||
#############################################
|
||||
###############################################################################
|
||||
# Output processing
|
||||
###############################################################################
|
||||
|
||||
output_processing = parser.add_argument_group(title='Output processing')
|
||||
|
||||
parser.add_argument(
|
||||
'--output', '-o', type=argparse.FileType('w+b'),
|
||||
output_processing.add_argument(
|
||||
'--output', '-o', type=FileType('w+b'),
|
||||
metavar='FILE',
|
||||
help= argparse.SUPPRESS if not is_windows else _(
|
||||
help=SUPPRESS if not is_windows else _(
|
||||
'''
|
||||
Save 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.
|
||||
|
||||
'''
|
||||
)
|
||||
)
|
||||
|
||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
||||
prettify.add_argument(
|
||||
'--pretty', dest='prettify', action='store_true',
|
||||
default=PRETTIFY_STDOUT_TTY_ONLY,
|
||||
output_processing.add_argument(
|
||||
'--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
help=_('''
|
||||
If stdout is a terminal, the response is prettified
|
||||
by default (colorized and indented if it is JSON).
|
||||
This flag ensures prettifying even when stdout is redirected.
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
and formatting
|
||||
(default for terminal output), "colors", or "format".
|
||||
''')
|
||||
)
|
||||
prettify.add_argument(
|
||||
'--ugly', '-u', dest='prettify', action='store_false',
|
||||
output_processing.add_argument(
|
||||
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
|
||||
choices=AVAILABLE_STYLES,
|
||||
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=_('''
|
||||
String specifying what the output should contain:
|
||||
"{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.
|
||||
If the output is piped to another program or to a file,
|
||||
then only the body is printed by default.
|
||||
'''.format(
|
||||
request_headers=OUT_REQ_HEAD,
|
||||
request_body=OUT_REQ_BODY,
|
||||
response_headers=OUT_RESP_HEAD,
|
||||
response_body=OUT_RESP_BODY,
|
||||
))
|
||||
'''.format(request_headers=OUT_REQ_HEAD,
|
||||
request_body=OUT_REQ_BODY,
|
||||
response_headers=OUT_RESP_HEAD,
|
||||
response_body=OUT_RESP_BODY,))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v', dest='output_options',
|
||||
@ -130,19 +198,9 @@ output_options.add_argument(
|
||||
'''.format(OUT_RESP_BODY))
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
|
||||
choices=AVAILABLE_STYLES,
|
||||
output_options.add_argument(
|
||||
'--stream', '-S', action='store_true', default=False,
|
||||
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'.
|
||||
|
||||
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
|
||||
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',
|
||||
help=_('''
|
||||
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
|
||||
(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.
|
||||
|
||||
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',
|
||||
help=_('''
|
||||
The authentication mechanism to be used.
|
||||
Defaults to "basic".
|
||||
''')
|
||||
)
|
||||
###############################################################################
|
||||
# Troubleshooting
|
||||
###############################################################################
|
||||
|
||||
parser.add_argument(
|
||||
'--verify', default='yes',
|
||||
help=_('''
|
||||
Set to "no" to skip checking the host\'s SSL certificate.
|
||||
You can also pass the path to a CA_BUNDLE
|
||||
file for private certs. You can also set
|
||||
the REQUESTS_CA_BUNDLE environment variable.
|
||||
Defaults to "yes".
|
||||
''')
|
||||
troubleshooting = parser.add_argument_group(title='Troubleshooting')
|
||||
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help', default=SUPPRESS,
|
||||
help='Show this help message and exit'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--proxy', default=[], action='append',
|
||||
type=KeyValueArgType(SEP_PROXY),
|
||||
help=_('''
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:foo.bar:3128).
|
||||
''')
|
||||
troubleshooting.add_argument(
|
||||
'--version', action='version', version=__version__)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback', action='store_true', default=False,
|
||||
help='Prints exception traceback should one occur.'
|
||||
)
|
||||
parser.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(
|
||||
troubleshooting.add_argument(
|
||||
'--debug', action='store_true', default=False,
|
||||
help=_('''
|
||||
Prints exception traceback should one occur and other
|
||||
information useful for debugging HTTPie itself.
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
# 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.
|
||||
Prints exception traceback should one occur, and also other
|
||||
information that is useful for debugging HTTPie itself and
|
||||
for bug reports.
|
||||
''')
|
||||
)
|
||||
|
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 json
|
||||
import errno
|
||||
|
||||
import requests
|
||||
import requests.auth
|
||||
from requests.compat import str
|
||||
from httpie import __version__ as httpie_version
|
||||
from requests import __version__ as requests_version
|
||||
from pygments import __version__ as pygments_version
|
||||
|
||||
from .cli import parser
|
||||
from .compat import str, is_py3
|
||||
from .client import get_response
|
||||
from .models import Environment
|
||||
from .output import output_stream, write
|
||||
from . import EXIT
|
||||
from .output import build_output_stream, write, write_with_colors_win_p3k
|
||||
from . import ExitStatus
|
||||
|
||||
|
||||
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
JSON = 'application/json; charset=utf-8'
|
||||
|
||||
|
||||
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:
|
||||
def get_exit_status(http_status, follow=False):
|
||||
"""Translate HTTP status code to exit status code."""
|
||||
if 300 <= http_status <= 399 and not follow:
|
||||
# Redirect
|
||||
return EXIT.ERROR_HTTP_3XX
|
||||
elif 400 <= code <= 499:
|
||||
return ExitStatus.ERROR_HTTP_3XX
|
||||
elif 400 <= http_status <= 499:
|
||||
# Client Error
|
||||
return EXIT.ERROR_HTTP_4XX
|
||||
elif 500 <= code <= 599:
|
||||
return ExitStatus.ERROR_HTTP_4XX
|
||||
elif 500 <= http_status <= 599:
|
||||
# Server Error
|
||||
return EXIT.ERROR_HTTP_5XX
|
||||
return ExitStatus.ERROR_HTTP_5XX
|
||||
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()):
|
||||
"""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
|
||||
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
|
||||
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:
|
||||
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:
|
||||
status = get_exist_status(response.status_code,
|
||||
args.allow_redirects)
|
||||
if status and not env.stdout_isatty:
|
||||
error('%s %s', response.raw.status, response.raw.reason)
|
||||
exit_status = get_exit_status(response.status_code, args.follow)
|
||||
|
||||
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:
|
||||
write(stream=stream,
|
||||
outfile=env.stdout,
|
||||
flush=env.stdout_isatty or args.stream)
|
||||
if env.is_windows and is_py3 and 'colors' in args.prettify:
|
||||
write_with_colors_win_p3k(**write_kwargs)
|
||||
else:
|
||||
write(**write_kwargs)
|
||||
|
||||
except IOError as e:
|
||||
if not debug and e.errno == errno.EPIPE:
|
||||
# Ignore broken pipes unless --debug.
|
||||
if not traceback and e.errno == errno.EPIPE:
|
||||
# Ignore broken pipes unless --traceback.
|
||||
env.stderr.write('\n')
|
||||
else:
|
||||
raise
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
if debug:
|
||||
if traceback:
|
||||
raise
|
||||
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:
|
||||
# TODO: distinguish between expected and unexpected errors.
|
||||
# network errors vs. bugs, etc.
|
||||
if debug:
|
||||
# TODO: Better distinction between expected and unexpected errors.
|
||||
# Network errors vs. bugs, etc.
|
||||
if traceback:
|
||||
raise
|
||||
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 re
|
||||
import json
|
||||
import argparse
|
||||
import mimetypes
|
||||
import getpass
|
||||
from io import BytesIO
|
||||
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
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.compat import str, urlparse
|
||||
|
||||
from . import __version__
|
||||
from .compat import urlsplit, str
|
||||
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
@ -66,15 +67,22 @@ OUTPUT_OPTIONS = frozenset([
|
||||
OUT_RESP_BODY
|
||||
])
|
||||
|
||||
# Pretty
|
||||
PRETTY_MAP = {
|
||||
'all': ['format', 'colors'],
|
||||
'colors': ['colors'],
|
||||
'format': ['format'],
|
||||
'none': []
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
PRETTIFY_STDOUT_TTY_ONLY = object()
|
||||
DEFAULT_UA = 'HTTPie/%s' % __version__
|
||||
|
||||
|
||||
class Parser(argparse.ArgumentParser):
|
||||
class Parser(ArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||
@ -85,27 +93,29 @@ class Parser(argparse.ArgumentParser):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['add_help'] = False
|
||||
super(Parser, self).__init__(*args, **kwargs)
|
||||
# Help only as --help (-h is used for --headers).
|
||||
self.add_argument('--help',
|
||||
action='help', default=argparse.SUPPRESS,
|
||||
help=argparse._('show this help message and exit'))
|
||||
|
||||
#noinspection PyMethodOverriding
|
||||
def parse_args(self, env, args=None, namespace=None):
|
||||
|
||||
self.env = env
|
||||
|
||||
if env.is_windows and not env.stdout_isatty:
|
||||
self.error('Output redirection is not supported on Windows.'
|
||||
' Please use `--output FILE\' instead.')
|
||||
args, no_options = super(Parser, self).parse_known_args(args,
|
||||
namespace)
|
||||
|
||||
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:
|
||||
env.stdout = args.output
|
||||
env.stdout_isatty = False
|
||||
|
||||
self._process_output_options(args, env)
|
||||
self._process_pretty_options(args, env)
|
||||
self._guess_method(args, env)
|
||||
self._parse_items(args)
|
||||
|
||||
@ -116,17 +126,54 @@ class Parser(argparse.ArgumentParser):
|
||||
scheme = HTTPS if env.progname == 'https' else HTTP
|
||||
args.url = scheme + args.url
|
||||
|
||||
if args.auth and not args.auth.has_password():
|
||||
# Stdin already read (if not a tty) so it's save to prompt.
|
||||
args.auth.prompt_password(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.')
|
||||
self._process_auth(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):
|
||||
# Sneak in our stderr/stdout.
|
||||
file = {
|
||||
@ -170,8 +217,8 @@ class Parser(argparse.ArgumentParser):
|
||||
args.items.insert(
|
||||
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
|
||||
|
||||
except argparse.ArgumentTypeError as e:
|
||||
if args.debug:
|
||||
except ArgumentTypeError as e:
|
||||
if args.traceback:
|
||||
raise
|
||||
self.error(e.message)
|
||||
|
||||
@ -189,7 +236,6 @@ class Parser(argparse.ArgumentParser):
|
||||
|
||||
"""
|
||||
args.headers = CaseInsensitiveDict()
|
||||
args.headers['User-Agent'] = DEFAULT_UA
|
||||
args.data = ParamDict() if args.form else OrderedDict()
|
||||
args.files = OrderedDict()
|
||||
args.params = ParamDict()
|
||||
@ -201,7 +247,7 @@ class Parser(argparse.ArgumentParser):
|
||||
files=args.files,
|
||||
params=args.params)
|
||||
except ParseError as e:
|
||||
if args.debug:
|
||||
if args.traceback:
|
||||
raise
|
||||
self.error(e.message)
|
||||
|
||||
@ -238,6 +284,14 @@ class Parser(argparse.ArgumentParser):
|
||||
if 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):
|
||||
pass
|
||||
@ -256,6 +310,38 @@ class KeyValue(object):
|
||||
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):
|
||||
"""A key-value pair argument type used with `argparse`.
|
||||
|
||||
@ -336,7 +422,7 @@ class KeyValueArgType(object):
|
||||
break
|
||||
|
||||
else:
|
||||
raise argparse.ArgumentTypeError(
|
||||
raise ArgumentTypeError(
|
||||
'"%s" is not a valid value' % string)
|
||||
|
||||
return self.key_value_class(
|
||||
@ -375,7 +461,7 @@ class AuthCredentialsArgType(KeyValueArgType):
|
||||
"""
|
||||
try:
|
||||
return super(AuthCredentialsArgType, self).__call__(string)
|
||||
except argparse.ArgumentTypeError:
|
||||
except ArgumentTypeError:
|
||||
# No password provided, will prompt for it later.
|
||||
return self.key_value_class(
|
||||
key=string,
|
||||
@ -397,9 +483,6 @@ class ParamDict(OrderedDict):
|
||||
data and URL params.
|
||||
|
||||
"""
|
||||
# NOTE: Won't work when used for form data with multiple values
|
||||
# for a field and a file field is present:
|
||||
# https://github.com/kennethreitz/requests/issues/737
|
||||
if key not in self:
|
||||
super(ParamDict, self).__setitem__(key, value)
|
||||
else:
|
||||
|
105
httpie/models.py
105
httpie/models.py
@ -1,6 +1,8 @@
|
||||
import os
|
||||
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):
|
||||
@ -11,21 +13,24 @@ class Environment(object):
|
||||
|
||||
"""
|
||||
|
||||
#noinspection PyUnresolvedReferences
|
||||
is_windows = is_windows
|
||||
|
||||
progname = os.path.basename(sys.argv[0])
|
||||
if progname not in ['http', 'https']:
|
||||
progname = 'http'
|
||||
|
||||
if is_windows:
|
||||
import colorama.initialise
|
||||
colorama.initialise.init()
|
||||
|
||||
stdin_isatty = sys.stdin.isatty()
|
||||
stdin = sys.stdin
|
||||
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
|
||||
|
||||
# Can be set to 0 to disable colors completely.
|
||||
@ -36,6 +41,16 @@ class Environment(object):
|
||||
for attr in kwargs.keys())
|
||||
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):
|
||||
"""Abstract class for HTTP messages."""
|
||||
@ -82,19 +97,28 @@ class HTTPResponse(HTTPMessage):
|
||||
return self._orig.iter_content(chunk_size=chunk_size)
|
||||
|
||||
def iter_lines(self, chunk_size):
|
||||
for line in self._orig.iter_lines(chunk_size):
|
||||
yield line, b'\n'
|
||||
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
original = self._orig.raw._original_response
|
||||
status_line = 'HTTP/{version} {status} {reason}'.format(
|
||||
version='.'.join(str(original.version)),
|
||||
status=original.status,
|
||||
reason=original.reason
|
||||
)
|
||||
headers = str(original.msg)
|
||||
return '\n'.join([status_line, headers]).strip()
|
||||
version='.'.join(str(original.version)),
|
||||
status=original.status,
|
||||
reason=original.reason
|
||||
)
|
||||
headers = [status_line]
|
||||
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
|
||||
def encoding(self):
|
||||
@ -118,40 +142,25 @@ class HTTPRequest(HTTPMessage):
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Return Request-Line"""
|
||||
url = urlparse(self._orig.url)
|
||||
url = urlsplit(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(
|
||||
method=self._orig.method,
|
||||
path=url.path or '/',
|
||||
query=qs
|
||||
query='?' + url.query if url.query else ''
|
||||
)
|
||||
|
||||
headers = dict(self._orig.headers)
|
||||
|
||||
if 'Host' not in headers:
|
||||
headers['Host'] = urlparse(self._orig.url).netloc
|
||||
headers['Host'] = url.netloc
|
||||
|
||||
headers = ['%s: %s' % (name, value)
|
||||
for name, value in headers.items()]
|
||||
|
||||
headers.insert(0, request_line)
|
||||
|
||||
return '\n'.join(headers).strip()
|
||||
return '\r\n'.join(headers).strip()
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
@ -159,26 +168,8 @@ class HTTPRequest(HTTPMessage):
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""Reconstruct and return the original request body bytes."""
|
||||
if self._orig.files:
|
||||
# TODO: would be nice if we didn't need to encode the files again
|
||||
# FIXME: Also the boundary header doesn't match the one used.
|
||||
for fn, fd in self._orig.files.values():
|
||||
# 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
|
||||
body = self._orig.body
|
||||
if isinstance(body, str):
|
||||
# Happens with JSON/form request data parsed from the command line.
|
||||
body = body.encode('utf8')
|
||||
return body or b''
|
||||
|
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.terminal256 import Terminal256Formatter
|
||||
from pygments.util import ClassNotFound
|
||||
from requests.compat import is_windows
|
||||
|
||||
from .compat import is_windows
|
||||
from .solarized import Solarized256Style
|
||||
from .models import HTTPRequest, HTTPResponse, Environment
|
||||
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||
|
||||
|
||||
# Colors on Windows via colorama aren't that great and fruity
|
||||
# seems to give the best result there.
|
||||
# Colors on Windows via colorama don't look that
|
||||
# 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'
|
||||
|
||||
#noinspection PySetFunctionToLiteral
|
||||
AVAILABLE_STYLES = set([DEFAULT_STYLE]) | set(STYLE_MAP.keys())
|
||||
|
||||
|
||||
BINARY_SUPPRESSED_NOTICE = (
|
||||
b'\n'
|
||||
@ -62,23 +61,38 @@ def write(stream, 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`
|
||||
exchange each of which yields `bytes` chunks.
|
||||
|
||||
"""
|
||||
|
||||
Stream = make_stream(env, args)
|
||||
|
||||
req_h = OUT_REQ_HEAD in args.output_options
|
||||
req_b = OUT_REQ_BODY 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
|
||||
resp = resp_h or resp_b
|
||||
|
||||
output = []
|
||||
Stream = get_stream_type(env, args)
|
||||
|
||||
if req:
|
||||
output.append(Stream(
|
||||
@ -86,8 +100,9 @@ def output_stream(args, env, request, response):
|
||||
with_headers=req_h,
|
||||
with_body=req_b))
|
||||
|
||||
if req and resp:
|
||||
output.append([b'\n\n\n'])
|
||||
if req_b and resp:
|
||||
# Request/Response separator.
|
||||
output.append([b'\n\n'])
|
||||
|
||||
if resp:
|
||||
output.append(Stream(
|
||||
@ -95,13 +110,15 @@ def output_stream(args, env, request, response):
|
||||
with_headers=resp_h,
|
||||
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'])
|
||||
|
||||
return chain(*output)
|
||||
|
||||
|
||||
def make_stream(env, args):
|
||||
def get_stream_type(env, args):
|
||||
"""Pick the right stream type based on `env` and `args`.
|
||||
Wrap it in a partial with the type-specific args so that
|
||||
we don't need to think what stream we are dealing with.
|
||||
@ -112,12 +129,15 @@ def make_stream(env, args):
|
||||
RawStream,
|
||||
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
||||
if args.stream
|
||||
else RawStream.CHUNK_SIZE)
|
||||
else RawStream.CHUNK_SIZE
|
||||
)
|
||||
elif args.prettify:
|
||||
Stream = partial(
|
||||
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:
|
||||
Stream = partial(EncodedStream, env=env)
|
||||
|
||||
@ -125,7 +145,7 @@ def make_stream(env, args):
|
||||
|
||||
|
||||
class BaseStream(object):
|
||||
"""Base HTTP message stream class."""
|
||||
"""Base HTTP message output stream class."""
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
assert with_headers or with_body
|
||||
self.msg = msg
|
||||
self.with_headers = with_headers
|
||||
self.with_body = with_body
|
||||
|
||||
def _headers(self):
|
||||
def _get_headers(self):
|
||||
"""Return the headers' bytes."""
|
||||
return self.msg.headers.encode('ascii')
|
||||
|
||||
def _body(self):
|
||||
def _iter_body(self):
|
||||
"""Return an iterator over the message body."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __iter__(self):
|
||||
"""Return an iterator over `self.msg`."""
|
||||
if self.with_headers:
|
||||
yield self._headers()
|
||||
yield self._get_headers()
|
||||
yield b'\r\n\r\n'
|
||||
|
||||
if self.with_body:
|
||||
it = self._body()
|
||||
|
||||
try:
|
||||
if self.with_headers:
|
||||
# Yield the headers/body separator only if needed.
|
||||
chunk = next(it)
|
||||
if chunk:
|
||||
yield b'\n\n'
|
||||
yield chunk
|
||||
|
||||
for chunk in it:
|
||||
for chunk in self._iter_body():
|
||||
yield chunk
|
||||
|
||||
except BinarySuppressedError as e:
|
||||
if self.with_headers:
|
||||
yield b'\n'
|
||||
@ -181,7 +193,7 @@ class RawStream(BaseStream):
|
||||
super(RawStream, self).__init__(**kwargs)
|
||||
self.chunk_size = chunk_size
|
||||
|
||||
def _body(self):
|
||||
def _iter_body(self):
|
||||
return self.msg.iter_body(self.chunk_size)
|
||||
|
||||
|
||||
@ -194,6 +206,7 @@ class EncodedStream(BaseStream):
|
||||
|
||||
"""
|
||||
CHUNK_SIZE = 1024 * 5
|
||||
|
||||
def __init__(self, env=Environment(), **kwargs):
|
||||
|
||||
super(EncodedStream, self).__init__(**kwargs)
|
||||
@ -208,7 +221,7 @@ class EncodedStream(BaseStream):
|
||||
# Default to utf8 when unsure.
|
||||
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):
|
||||
|
||||
@ -234,11 +247,11 @@ class PrettyStream(EncodedStream):
|
||||
super(PrettyStream, self).__init__(**kwargs)
|
||||
self.processor = processor
|
||||
|
||||
def _headers(self):
|
||||
def _get_headers(self):
|
||||
return self.processor.process_headers(
|
||||
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):
|
||||
if b'\0' in line:
|
||||
raise BinarySuppressedError()
|
||||
@ -262,9 +275,8 @@ class BufferedPrettyStream(PrettyStream):
|
||||
|
||||
CHUNK_SIZE = 1024 * 10
|
||||
|
||||
def _body(self):
|
||||
def _iter_body(self):
|
||||
|
||||
#noinspection PyArgumentList
|
||||
# Read the whole body before prettifying it,
|
||||
# but bail out immediately if the body is binary.
|
||||
body = bytearray()
|
||||
@ -297,13 +309,13 @@ class HTTPLexer(lexer.RegexLexer):
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
lexer.bygroups(
|
||||
token.Name.Function,
|
||||
token.Text,
|
||||
token.Name.Namespace,
|
||||
token.Text,
|
||||
token.Keyword.Reserved,
|
||||
token.Operator,
|
||||
token.Number
|
||||
token.Name.Function,
|
||||
token.Text,
|
||||
token.Name.Namespace,
|
||||
token.Text,
|
||||
token.Keyword.Reserved,
|
||||
token.Operator,
|
||||
token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||
@ -318,13 +330,14 @@ class HTTPLexer(lexer.RegexLexer):
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
|
||||
token.Name.Attribute, # Name
|
||||
token.Name.Attribute, # Name
|
||||
token.Text,
|
||||
token.Operator, # Colon
|
||||
token.Text,
|
||||
token.String # Value
|
||||
))
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class BaseProcessor(object):
|
||||
@ -332,12 +345,11 @@ class BaseProcessor(object):
|
||||
|
||||
enabled = True
|
||||
|
||||
def __init__(self, env, **kwargs):
|
||||
def __init__(self, env=Environment(), **kwargs):
|
||||
"""
|
||||
:param env:
|
||||
an class:`Environment` instance
|
||||
:param kwargs:
|
||||
additional keyword argument that some processor might require.
|
||||
:param env: an class:`Environment` instance
|
||||
:param kwargs: additional keyword argument that some
|
||||
processor might require.
|
||||
|
||||
"""
|
||||
self.env = env
|
||||
@ -346,8 +358,7 @@ class BaseProcessor(object):
|
||||
def process_headers(self, headers):
|
||||
"""Return processed `headers`
|
||||
|
||||
:param headers:
|
||||
The headers as text.
|
||||
:param headers: The headers as text.
|
||||
|
||||
"""
|
||||
return headers
|
||||
@ -355,14 +366,9 @@ class BaseProcessor(object):
|
||||
def process_body(self, content, content_type, subtype):
|
||||
"""Return processed `content`.
|
||||
|
||||
:param content:
|
||||
The body content as text
|
||||
|
||||
:param content_type:
|
||||
Full content type, e.g., 'application/atom+xml'.
|
||||
|
||||
:param subtype:
|
||||
E.g. 'xml'.
|
||||
:param content: The body content as text
|
||||
:param content_type: Full content type, e.g., 'application/atom+xml'.
|
||||
:param subtype: E.g. 'xml'.
|
||||
|
||||
"""
|
||||
return content
|
||||
@ -402,7 +408,8 @@ class PygmentsProcessor(BaseProcessor):
|
||||
return
|
||||
|
||||
try:
|
||||
style = get_style_by_name(self.kwargs['pygments_style'])
|
||||
style = get_style_by_name(
|
||||
self.kwargs.get('pygments_style', DEFAULT_STYLE))
|
||||
except ClassNotFound:
|
||||
style = Solarized256Style
|
||||
|
||||
@ -440,24 +447,35 @@ class HeadersProcessor(BaseProcessor):
|
||||
def process_headers(self, headers):
|
||||
lines = headers.splitlines()
|
||||
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):
|
||||
"""A delegate class that invokes the actual processors."""
|
||||
|
||||
installed_processors = [
|
||||
JSONProcessor,
|
||||
HeadersProcessor,
|
||||
PygmentsProcessor
|
||||
]
|
||||
|
||||
def __init__(self, env, **kwargs):
|
||||
processors = [
|
||||
cls(env, **kwargs)
|
||||
for cls in self.installed_processors
|
||||
installed_processors = {
|
||||
'format': [
|
||||
HeadersProcessor,
|
||||
JSONProcessor
|
||||
],
|
||||
'colors': [
|
||||
PygmentsProcessor
|
||||
]
|
||||
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):
|
||||
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 @@
|
||||
#
|
16
setup.py
16
setup.py
@ -1,5 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import codecs
|
||||
from setuptools import setup
|
||||
import httpie
|
||||
|
||||
@ -10,8 +12,7 @@ if sys.argv[-1] == 'test':
|
||||
|
||||
|
||||
requirements = [
|
||||
# Debian has only requests==0.10.1 and httpie.deb depends on that.
|
||||
'requests>=0.10.1',
|
||||
'requests>=1.0.4',
|
||||
'Pygments>=1.5'
|
||||
]
|
||||
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')
|
||||
|
||||
|
||||
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(
|
||||
name='httpie',
|
||||
version=httpie.__version__,
|
||||
description=httpie.__doc__.strip(),
|
||||
long_description=open('README.rst').read(),
|
||||
long_description=long_description(),
|
||||
url='http://httpie.org/',
|
||||
download_url='https://github.com/jkbr/httpie',
|
||||
author=httpie.__author__,
|
||||
|
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
|
||||
|
||||
"""
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import tempfile
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from urllib.request import urlopen
|
||||
except ImportError:
|
||||
from urllib2 import urlopen
|
||||
try:
|
||||
from unittest import skipIf
|
||||
from unittest import skipIf, skip
|
||||
except ImportError:
|
||||
skip = lambda msg: lambda self: None
|
||||
|
||||
def skipIf(cond, reason):
|
||||
def decorator(test_method):
|
||||
@ -40,9 +44,7 @@ except ImportError:
|
||||
return test_method
|
||||
return decorator
|
||||
|
||||
|
||||
from requests.compat import is_windows, is_py26, bytes, str
|
||||
|
||||
from requests import __version__ as requests_version
|
||||
|
||||
|
||||
#################################################################
|
||||
@ -53,14 +55,16 @@ from requests.compat import is_windows, is_py26, bytes, str
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
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.models import Environment
|
||||
from httpie.core import main
|
||||
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
||||
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',
|
||||
'http://httpbin.org').rstrip('/')
|
||||
|
||||
@ -97,11 +101,16 @@ def httpbin(path):
|
||||
return HTTPBIN_URL + path
|
||||
|
||||
|
||||
def mk_config_dir():
|
||||
return tempfile.mkdtemp(prefix='httpie_test_config_dir_')
|
||||
|
||||
|
||||
class TestEnvironment(Environment):
|
||||
colors = 0
|
||||
stdin_isatty = True,
|
||||
stdout_isatty = True
|
||||
is_windows = False
|
||||
_shutil = shutil # we need it in __del__ (would get gc'd)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
@ -111,11 +120,43 @@ class TestEnvironment(Environment):
|
||||
if 'stderr' not in kwargs:
|
||||
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)
|
||||
|
||||
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):
|
||||
stderr = json = exit_status = None
|
||||
|
||||
|
||||
class StrResponse(str):
|
||||
stderr = json = exit_status = None
|
||||
|
||||
@ -142,18 +183,17 @@ def http(*args, **kwargs):
|
||||
try:
|
||||
|
||||
try:
|
||||
exit_status = main(args=['--debug'] + list(args), **kwargs)
|
||||
exit_status = main(args=['--traceback'] + list(args), **kwargs)
|
||||
except Exception:
|
||||
sys.stderr.write(env.stderr.read())
|
||||
raise
|
||||
except SystemExit:
|
||||
exit_status = EXIT.ERROR
|
||||
exit_status = ExitStatus.ERROR
|
||||
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
|
||||
output = env.stdout.read()
|
||||
|
||||
try:
|
||||
r = StrResponse(output.decode('utf8'))
|
||||
except UnicodeDecodeError:
|
||||
@ -166,7 +206,7 @@ def http(*args, **kwargs):
|
||||
r.json = json.loads(r)
|
||||
elif r.count('Content-Type:') == 1 and 'application/json' in r:
|
||||
try:
|
||||
j = r.strip()[r.strip().rindex('\n\n'):]
|
||||
j = r.strip()[r.strip().rindex('\r\n\r\n'):]
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
@ -187,6 +227,8 @@ def http(*args, **kwargs):
|
||||
|
||||
class BaseTestCase(unittest.TestCase):
|
||||
|
||||
maxDiff = 100000
|
||||
|
||||
if is_py26:
|
||||
def assertIn(self, member, container, msg=None):
|
||||
self.assertTrue(member in container, msg)
|
||||
@ -502,8 +544,8 @@ class ImplicitHTTPMethodTest(BaseTestCase):
|
||||
self.assertIn(OK, r)
|
||||
|
||||
|
||||
class PrettyFlagTest(BaseTestCase):
|
||||
"""Test the --pretty / --ugly flag handling."""
|
||||
class PrettyOptionsTest(BaseTestCase):
|
||||
"""Test the --pretty flag handling."""
|
||||
|
||||
def test_pretty_enabled_by_default(self):
|
||||
r = http(
|
||||
@ -522,7 +564,7 @@ class PrettyFlagTest(BaseTestCase):
|
||||
|
||||
def test_force_pretty(self):
|
||||
r = http(
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=TestEnvironment(stdout_isatty=False, colors=256),
|
||||
@ -531,7 +573,7 @@ class PrettyFlagTest(BaseTestCase):
|
||||
|
||||
def test_force_ugly(self):
|
||||
r = http(
|
||||
'--ugly',
|
||||
'--pretty=none',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
)
|
||||
@ -544,7 +586,7 @@ class PrettyFlagTest(BaseTestCase):
|
||||
"""
|
||||
r = http(
|
||||
'--print=B',
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
httpbin('/post'),
|
||||
'Content-Type:text/foo+json',
|
||||
'a=b',
|
||||
@ -552,6 +594,34 @@ class PrettyFlagTest(BaseTestCase):
|
||||
)
|
||||
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):
|
||||
|
||||
@ -688,7 +758,7 @@ class BinaryResponseDataTest(BaseTestCase):
|
||||
|
||||
def test_binary_suppresses_when_not_terminal_but_pretty(self):
|
||||
r = http(
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
'GET',
|
||||
self.url,
|
||||
env=TestEnvironment(stdin_isatty=True,
|
||||
@ -765,6 +835,8 @@ class AuthTest(BaseTestCase):
|
||||
self.assertIn('"authenticated": true', 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):
|
||||
r = http(
|
||||
'--auth-type=digest',
|
||||
@ -791,6 +863,31 @@ class AuthTest(BaseTestCase):
|
||||
self.assertIn('"authenticated": true', 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):
|
||||
|
||||
@ -800,7 +897,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/200')
|
||||
)
|
||||
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):
|
||||
r = http(
|
||||
@ -808,7 +905,16 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/500')
|
||||
)
|
||||
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):
|
||||
r = http(
|
||||
@ -819,19 +925,21 @@ class ExitStatusTest(BaseTestCase):
|
||||
env=TestEnvironment(stdout_isatty=False,)
|
||||
)
|
||||
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())
|
||||
|
||||
@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):
|
||||
r = http(
|
||||
'--check-status',
|
||||
'--allow-redirects',
|
||||
'--follow',
|
||||
'GET',
|
||||
httpbin('/status/301')
|
||||
)
|
||||
# The redirect will be followed so 200 is expected.
|
||||
self.assertIn('HTTP/1.1 200 OK', r)
|
||||
self.assertEqual(r.exit_status, EXIT.OK)
|
||||
self.assertEqual(r.exit_status, ExitStatus.OK)
|
||||
|
||||
def test_4xx_check_status_exits_4(self):
|
||||
r = http(
|
||||
@ -840,7 +948,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/401')
|
||||
)
|
||||
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.
|
||||
self.assertTrue(not r.stderr)
|
||||
|
||||
@ -851,34 +959,32 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/500')
|
||||
)
|
||||
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):
|
||||
|
||||
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):
|
||||
|
||||
r = http(
|
||||
'--output',
|
||||
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=TestEnvironment(is_windows=True)
|
||||
)
|
||||
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):
|
||||
@ -890,7 +996,7 @@ class StreamTest(BaseTestCase):
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
'--stream',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
@ -906,10 +1012,11 @@ class StreamTest(BaseTestCase):
|
||||
#self.assertIn(OK_COLOR, r)
|
||||
|
||||
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:
|
||||
r = http(
|
||||
'--ugly',
|
||||
'--pretty=none',
|
||||
'--stream',
|
||||
'--verbose',
|
||||
'GET',
|
||||
@ -924,10 +1031,11 @@ class StreamTest(BaseTestCase):
|
||||
#self.assertIn(OK, r)
|
||||
|
||||
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:
|
||||
r = http(
|
||||
'--ugly',
|
||||
'--pretty=none',
|
||||
'--stream',
|
||||
'--verbose',
|
||||
'GET',
|
||||
@ -943,6 +1051,67 @@ class StreamTest(BaseTestCase):
|
||||
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.
|
||||
#################################################################
|
||||
@ -1093,11 +1262,153 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
self.assertEqual(args.items, [
|
||||
input.KeyValue(
|
||||
key='new_item', value='a', sep='=', orig='new_item=a'),
|
||||
input.KeyValue(key
|
||||
='old_item', value='b', sep='=', orig='old_item=b'),
|
||||
input.KeyValue(
|
||||
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__':
|
||||
#noinspection PyCallingNonCallable
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user