mirror of
https://github.com/httpie/cli.git
synced 2025-08-13 23:49:57 +02:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
f7e62336db | |||
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 |
321
README.rst
321
README.rst
@ -2,10 +2,10 @@
|
||||
HTTPie: cURL for Humans
|
||||
***********************
|
||||
|
||||
v0.2.7
|
||||
v0.3.0
|
||||
|
||||
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
|
||||
with web services as **human-friendly** as possible. It provides a
|
||||
simple ``http`` command that allows for sending arbitrary HTTP requests with a
|
||||
simple and natural syntax, and displays colorized responses. HTTPie can be used
|
||||
for **testing, debugging**, and generally **interacting** with HTTP servers.
|
||||
@ -18,7 +18,7 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
|
||||
|
||||
|
||||
HTTPie is written in Python, and under the hood it uses the excellent
|
||||
`Requests`_ and `Pygments`_ libraries.
|
||||
`Requests`_ for HTTP and `Pygments`_ for colorizing.
|
||||
|
||||
|
||||
**Table of Contents**
|
||||
@ -39,10 +39,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
|
||||
@ -80,12 +81,13 @@ Or, you can install the **development version** directly from GitHub:
|
||||
|
||||
|
||||
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
|
||||
Linux distributions as well.
|
||||
Linux distributions as well. However, they may be a significant delay between
|
||||
releases and package updates.
|
||||
|
||||
|
||||
===========
|
||||
Quick Start
|
||||
===========
|
||||
=====
|
||||
Usage
|
||||
=====
|
||||
|
||||
|
||||
Hello World:
|
||||
@ -111,53 +113,54 @@ 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 on 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
|
||||
|
||||
|
||||
--------
|
||||
|
||||
*What follows is a detailed documentation. It covers the command syntax,
|
||||
advances usage, and also features additional examples.*
|
||||
|
||||
|
||||
============
|
||||
HTTP Method
|
||||
============
|
||||
@ -169,7 +172,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 +180,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).
|
||||
|
||||
|
||||
===========
|
||||
@ -269,7 +250,7 @@ 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
|
||||
pair (``foo=`` and ``bar``) instead of a URL parameter.
|
||||
|
||||
No that data fields aren't the only way to specify request data,
|
||||
Note that data fields aren't the only way to specify request data,
|
||||
`redirected input`_ allows passing arbitrary data to be sent with the request.
|
||||
|
||||
|
||||
@ -278,7 +259,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 +270,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 +323,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 +336,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 via the `config`_
|
||||
file.
|
||||
|
||||
|
||||
-------------
|
||||
Regular Forms
|
||||
-------------
|
||||
@ -374,7 +365,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 +407,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 +418,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
|
||||
@ -447,6 +440,7 @@ come). There are two flags that control authorization:
|
||||
``basic`` so it can often be omitted.
|
||||
=================== ======================================================
|
||||
|
||||
Authorization information from ``.netrc`` is respected as well.
|
||||
|
||||
Basic auth:
|
||||
|
||||
@ -471,6 +465,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 +565,7 @@ Character Stands for
|
||||
``b`` Response body.
|
||||
========== ==================
|
||||
|
||||
Print both the request and response headers:
|
||||
Print request and response headers:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -547,14 +582,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 +638,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 +670,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 +683,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 +692,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 +749,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 +771,16 @@ 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.
|
||||
|
||||
|
||||
==================
|
||||
@ -737,7 +789,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 +800,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 +811,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
|
||||
|
||||
@ -769,6 +821,72 @@ Streamed output by small chunks:
|
||||
| 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 supports persistent sessions, where custom headers, authorization,
|
||||
and cookies (manually specified or sent by the server) persist between
|
||||
requests. Sessions are named and host-bound.
|
||||
|
||||
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:
|
||||
|
||||
.. 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.
|
||||
|
||||
Sessions are stored as JSON files in ``~/.httpie/sessions/<host>/<name>.json``
|
||||
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||
|
||||
See also `config`_.
|
||||
|
||||
|
||||
======
|
||||
Config
|
||||
======
|
||||
|
||||
HTTPie uses a simple configuration file that contains a JSON object with the
|
||||
following keys:
|
||||
|
||||
========================= =================================================
|
||||
``__version__`` HTTPie automatically sets this to its version.
|
||||
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.
|
||||
========================= =================================================
|
||||
|
||||
The default location is ``~/.httpie/config.json``
|
||||
(``%APPDATA%\httpie\config.json`` on Windows).
|
||||
|
||||
The config directory location can be changed by setting the
|
||||
``HTTPIE_CONFIG_DIR`` environment variable.
|
||||
|
||||
|
||||
=========
|
||||
Scripting
|
||||
=========
|
||||
@ -776,17 +894,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 +951,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 +994,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,7 +1003,7 @@ 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`_.
|
||||
|
||||
|
||||
=======
|
||||
@ -900,14 +1025,28 @@ Please see `LICENSE`_.
|
||||
Changelog
|
||||
=========
|
||||
|
||||
* `0.2.8dev`_
|
||||
*You can click a version name to see a diff with the previous one.*
|
||||
|
||||
* `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.
|
||||
@ -980,7 +1119,7 @@ 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
|
||||
.. _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
|
||||
|
@ -3,14 +3,16 @@ HTTPie - cURL for humans.
|
||||
|
||||
"""
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__version__ = '0.2.7'
|
||||
__version__ = '0.3.0'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
class EXIT:
|
||||
class exit:
|
||||
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
|
||||
|
352
httpie/cli.py
352
httpie/cli.py
@ -1,20 +1,23 @@
|
||||
"""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 argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
|
||||
from requests.compat import is_windows
|
||||
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from .sessions import DEFAULT_SESSIONS_DIR
|
||||
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)
|
||||
|
||||
|
||||
def _(text):
|
||||
@ -22,15 +25,73 @@ 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=_('''
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
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
|
||||
).add_mutually_exclusive_group(required=False)
|
||||
|
||||
content_type.add_argument(
|
||||
'--json', '-j', action='store_true',
|
||||
help=_('''
|
||||
(default) Data items from the command
|
||||
@ -39,55 +100,68 @@ 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_print = output_options.add_mutually_exclusive_group(required=False)
|
||||
output_print.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
|
||||
@ -105,7 +179,7 @@ output_options.add_argument('--print', '-p', dest='output_options',
|
||||
response_body=OUT_RESP_BODY,
|
||||
))
|
||||
)
|
||||
output_options.add_argument(
|
||||
output_print.add_argument(
|
||||
'--verbose', '-v', dest='output_options',
|
||||
action='store_const', const=''.join(OUTPUT_OPTIONS),
|
||||
help=_('''
|
||||
@ -113,7 +187,7 @@ output_options.add_argument(
|
||||
Shortcut for --print={0}.
|
||||
'''.format(''.join(OUTPUT_OPTIONS)))
|
||||
)
|
||||
output_options.add_argument(
|
||||
output_print.add_argument(
|
||||
'--headers', '-h', dest='output_options',
|
||||
action='store_const', const=OUT_RESP_HEAD,
|
||||
help=_('''
|
||||
@ -121,7 +195,7 @@ output_options.add_argument(
|
||||
Shortcut for --print={0}.
|
||||
'''.format(OUT_RESP_HEAD))
|
||||
)
|
||||
output_options.add_argument(
|
||||
output_print.add_argument(
|
||||
'--body', '-b', dest='output_options',
|
||||
action='store_const', const=OUT_RESP_BODY,
|
||||
help=_('''
|
||||
@ -130,19 +204,8 @@ 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),
|
||||
@ -156,7 +219,96 @@ parser.add_argument('--stream', '-S', action='store_true', default=False, help=_
|
||||
|
||||
'''
|
||||
))
|
||||
parser.add_argument(
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Sessions
|
||||
###############################################################################
|
||||
sessions = parser.add_argument_group(title='Sessions')\
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
|
||||
sessions.add_argument(
|
||||
'--session', metavar='SESSION_NAME',
|
||||
help=_('''
|
||||
Create, or reuse and update a session.
|
||||
Withing 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 +319,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 +327,28 @@ 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.
|
||||
''')
|
||||
)
|
||||
|
88
httpie/client.py
Normal file
88
httpie/client.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
import sys
|
||||
from pprint import pformat
|
||||
|
||||
import requests
|
||||
import requests.auth
|
||||
from requests.defaults import defaults
|
||||
|
||||
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:
|
||||
return requests.request(**requests_kwargs)
|
||||
else:
|
||||
return 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),
|
||||
)
|
||||
|
||||
|
||||
def get_requests_kwargs(args):
|
||||
"""Translate our `args` into `requests.request` keyword arguments."""
|
||||
|
||||
base_headers = defaults['base_headers'].copy()
|
||||
base_headers['User-Agent'] = DEFAULT_UA
|
||||
|
||||
auto_json = args.data and not args.form
|
||||
if args.json or auto_json:
|
||||
base_headers['Accept'] = 'application/json'
|
||||
if args.data:
|
||||
base_headers['Content-Type'] = 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 and not args.files:
|
||||
# If sending files, `requests` will set
|
||||
# the `Content-Type` for us.
|
||||
base_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)
|
||||
|
||||
kwargs = {
|
||||
'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.follow,
|
||||
'params': args.params,
|
||||
'config': {
|
||||
'base_headers': base_headers
|
||||
}
|
||||
}
|
||||
|
||||
return kwargs
|
83
httpie/config.py
Normal file
83
httpie/config.py
Normal file
@ -0,0 +1,83 @@
|
||||
import os
|
||||
import json
|
||||
import errno
|
||||
|
||||
from . import __version__
|
||||
from requests.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
|
||||
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['__version__'] = __version__
|
||||
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'
|
||||
|
||||
DEFAULTS = {
|
||||
'implicit_content_type': 'json',
|
||||
'default_options': []
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Config, self).__init__(*args, **kwargs)
|
||||
self.update(self.DEFAULTS)
|
127
httpie/core.py
127
httpie/core.py
@ -11,82 +11,44 @@ Invocation flow:
|
||||
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import errno
|
||||
|
||||
import requests
|
||||
import requests.auth
|
||||
from requests.compat import str
|
||||
from requests.compat import str, is_py3
|
||||
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 .client import get_response
|
||||
from .models import Environment
|
||||
from .output import output_stream, write
|
||||
from . import EXIT
|
||||
from .output import output_stream, write, write_with_colors_win_p3k
|
||||
from . import exit
|
||||
|
||||
|
||||
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):
|
||||
def get_exist_status(code, follow=False):
|
||||
"""Translate HTTP status code to exit status."""
|
||||
if 300 <= code <= 399 and not allow_redirects:
|
||||
if 300 <= code <= 399 and not follow:
|
||||
# Redirect
|
||||
return EXIT.ERROR_HTTP_3XX
|
||||
return exit.ERROR_HTTP_3XX
|
||||
elif 400 <= code <= 499:
|
||||
# Client Error
|
||||
return EXIT.ERROR_HTTP_4XX
|
||||
return exit.ERROR_HTTP_4XX
|
||||
elif 500 <= code <= 599:
|
||||
# Server Error
|
||||
return EXIT.ERROR_HTTP_5XX
|
||||
return exit.ERROR_HTTP_5XX
|
||||
else:
|
||||
return EXIT.OK
|
||||
return exit.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()):
|
||||
@ -95,50 +57,67 @@ def main(args=sys.argv[1:], env=Environment()):
|
||||
Return exit status.
|
||||
|
||||
"""
|
||||
if env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
|
||||
def error(msg, *args):
|
||||
msg = msg % args
|
||||
env.stderr.write('\nhttp: error: %s\n' % msg)
|
||||
|
||||
debug = '--debug' in args
|
||||
status = EXIT.OK
|
||||
traceback = debug or '--traceback' in args
|
||||
status = exit.OK
|
||||
|
||||
if debug:
|
||||
print_debug_info(env)
|
||||
if args == ['--debug']:
|
||||
sys.exit(exit.OK)
|
||||
|
||||
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)
|
||||
args.follow)
|
||||
if status and not env.stdout_isatty:
|
||||
error('%s %s', response.raw.status, response.raw.reason)
|
||||
|
||||
stream = output_stream(args, env, response.request, response)
|
||||
|
||||
write_kwargs = {
|
||||
'stream': stream,
|
||||
'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
|
||||
|
||||
status = exit.ERROR
|
||||
except requests.Timeout:
|
||||
status = exit.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:
|
||||
if traceback:
|
||||
raise
|
||||
error('%s: %s', type(e).__name__, str(e))
|
||||
status = EXIT.ERROR
|
||||
status = exit.ERROR
|
||||
|
||||
return status
|
||||
|
@ -5,10 +5,10 @@ import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
import mimetypes
|
||||
import getpass
|
||||
from io import BytesIO
|
||||
from argparse import ArgumentParser, ArgumentTypeError
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
@ -18,8 +18,6 @@ except ImportError:
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from requests.compat import str, urlparse
|
||||
|
||||
from . import __version__
|
||||
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
@ -66,15 +64,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 +90,26 @@ 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 = super(Parser, self).parse_args(args, namespace)
|
||||
|
||||
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)
|
||||
|
||||
@ -120,11 +124,6 @@ class Parser(argparse.ArgumentParser):
|
||||
# Stdin already read (if not a tty) so it's save to prompt.
|
||||
args.auth.prompt_password(urlparse(args.url).netloc)
|
||||
|
||||
if args.prettify == PRETTIFY_STDOUT_TTY_ONLY:
|
||||
args.prettify = env.stdout_isatty
|
||||
elif args.prettify and env.is_windows:
|
||||
self.error('Only terminal output can be prettified on Windows.')
|
||||
|
||||
return args
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
@ -170,8 +169,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 +188,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 +199,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 +236,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
|
||||
@ -336,7 +342,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 +381,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,
|
||||
|
30
httpie/manage.py
Normal file
30
httpie/manage.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""
|
||||
Provides the `httpie' management command.
|
||||
|
||||
Note that the main `http' command points to `httpie.__main__.main()`.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
|
||||
from . import sessions
|
||||
from . import __version__
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='The HTTPie management command.',
|
||||
version=__version__
|
||||
)
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
|
||||
# Only sessions as of now.
|
||||
sessions.add_commands(subparsers)
|
||||
|
||||
|
||||
def main():
|
||||
args = parser.parse_args()
|
||||
args.command(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -2,6 +2,8 @@ import os
|
||||
import sys
|
||||
from requests.compat import urlparse, is_windows, bytes, str
|
||||
|
||||
from .config import DEFAULT_CONFIG_DIR, Config
|
||||
|
||||
|
||||
class Environment(object):
|
||||
"""Holds information about the execution context.
|
||||
@ -18,13 +20,17 @@ class Environment(object):
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
@ -36,6 +42,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,8 +98,7 @@ 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):
|
||||
@ -93,8 +108,18 @@ class HTTPResponse(HTTPMessage):
|
||||
status=original.status,
|
||||
reason=original.reason
|
||||
)
|
||||
headers = str(original.msg)
|
||||
return '\n'.join([status_line, headers]).strip()
|
||||
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):
|
||||
@ -151,7 +176,7 @@ class HTTPRequest(HTTPMessage):
|
||||
|
||||
headers.insert(0, request_line)
|
||||
|
||||
return '\n'.join(headers).strip()
|
||||
return '\r\n'.join(headers).strip()
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
|
116
httpie/output.py
116
httpie/output.py
@ -20,13 +20,12 @@ 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,6 +61,23 @@ def write(stream, outfile, flush):
|
||||
outfile.flush()
|
||||
|
||||
|
||||
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 output_stream(args, env, request, response):
|
||||
"""Build and return a chain of iterators over the `request`-`response`
|
||||
exchange each of which yields `bytes` chunks.
|
||||
@ -86,8 +102,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,7 +112,9 @@ 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)
|
||||
@ -112,12 +131,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)
|
||||
|
||||
@ -150,21 +172,12 @@ class BaseStream(object):
|
||||
"""Return an iterator over `self.msg`."""
|
||||
if self.with_headers:
|
||||
yield self._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'
|
||||
for chunk in self._body():
|
||||
yield chunk
|
||||
|
||||
for chunk in it:
|
||||
yield chunk
|
||||
|
||||
except BinarySuppressedError as e:
|
||||
if self.with_headers:
|
||||
yield b'\n'
|
||||
@ -194,6 +207,7 @@ class EncodedStream(BaseStream):
|
||||
|
||||
"""
|
||||
CHUNK_SIZE = 1024 * 5
|
||||
|
||||
def __init__(self, env=Environment(), **kwargs):
|
||||
|
||||
super(EncodedStream, self).__init__(**kwargs)
|
||||
@ -324,7 +338,8 @@ class HTTPLexer(lexer.RegexLexer):
|
||||
token.Text,
|
||||
token.String # Value
|
||||
))
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class BaseProcessor(object):
|
||||
@ -332,12 +347,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 +360,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 +368,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 +410,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 +449,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,
|
||||
installed_processors = {
|
||||
'format': [
|
||||
HeadersProcessor,
|
||||
JSONProcessor
|
||||
],
|
||||
'colors': [
|
||||
PygmentsProcessor
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, env, **kwargs):
|
||||
processors = [
|
||||
cls(env, **kwargs)
|
||||
for cls in self.installed_processors
|
||||
]
|
||||
self.processors = [p for p in processors if p.enabled]
|
||||
def __init__(self, groups, env=Environment(), **kwargs):
|
||||
"""
|
||||
:param env: a :class:`models.Environment` instance
|
||||
:param groups: the groups of processors to be applied
|
||||
:param kwargs: additional keyword arguments for processors
|
||||
|
||||
"""
|
||||
self.processors = []
|
||||
for group in groups:
|
||||
for cls in self.installed_processors[group]:
|
||||
processor = cls(env, **kwargs)
|
||||
if processor.enabled:
|
||||
self.processors.append(processor)
|
||||
|
||||
def process_headers(self, headers):
|
||||
for processor in self.processors:
|
||||
|
233
httpie/sessions.py
Normal file
233
httpie/sessions.py
Normal file
@ -0,0 +1,233 @@
|
||||
"""Persistent, JSON-serialized sessions.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import errno
|
||||
import codecs
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
from requests.compat import urlparse
|
||||
from requests.cookies import RequestsCookieJar, create_cookie
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
from argparse import OPTIONAL
|
||||
|
||||
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||
from .output import PygmentsProcessor
|
||||
|
||||
|
||||
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 urlparse(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
|
||||
|
||||
rsession = requests.Session(cookies=session.cookies)
|
||||
try:
|
||||
response = rsession.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 = rsession.cookies
|
||||
session.save()
|
||||
return response
|
||||
|
||||
|
||||
class Host(object):
|
||||
"""A host is a per-host directory on the disk containing sessions files."""
|
||||
|
||||
def __init__(self, name, root_dir=DEFAULT_CONFIG_DIR):
|
||||
self.name = name
|
||||
self.root_dir = root_dir
|
||||
|
||||
def __iter__(self):
|
||||
"""Return a iterator yielding `(session_name, session_path)`."""
|
||||
for fn in sorted(glob.glob1(self.path, '*.json')):
|
||||
yield os.path.splitext(fn)[0], os.path.join(self.path, fn)
|
||||
|
||||
def delete(self):
|
||||
shutil.rmtree(self.path)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
# Name will include ':' if a port is specified, which is invalid
|
||||
# on windows. DNS does not allow '_' in a domain, or for it to end
|
||||
# in a number (I think?)
|
||||
path = os.path.join(self.root_dir, self.name.replace(':', '_'))
|
||||
try:
|
||||
os.makedirs(path, mode=0o700)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
"""Return a generator yielding a host at a time."""
|
||||
for name in sorted(glob.glob1(DEFAULT_SESSIONS_DIR, '*')):
|
||||
if os.path.isdir(os.path.join(DEFAULT_SESSIONS_DIR, name)):
|
||||
yield Host(name)
|
||||
|
||||
|
||||
class Session(BaseConfigDict):
|
||||
""""""
|
||||
|
||||
def __init__(self, host, name, *args, **kwargs):
|
||||
super(Session, self).__init__(*args, **kwargs)
|
||||
self.host = host
|
||||
self.name = name
|
||||
self['headers'] = {}
|
||||
self['cookies'] = {}
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
return self.host.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):
|
||||
excluded = [
|
||||
'_rest', 'name', 'port_specified',
|
||||
'domain_specified', 'domain_initial_dot',
|
||||
'path_specified', 'comment', 'comment_url'
|
||||
]
|
||||
self['cookies'] = {}
|
||||
for host in jar._cookies.values():
|
||||
for path in host.values():
|
||||
for name, cookie in path.items():
|
||||
cookie_dict = {}
|
||||
for k, v in cookie.__dict__.items():
|
||||
if k not in excluded:
|
||||
cookie_dict[k] = v
|
||||
self['cookies'][name] = cookie_dict
|
||||
|
||||
@property
|
||||
def auth(self):
|
||||
auth = self.get('auth', None)
|
||||
if not auth:
|
||||
return None
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# The commands are disabled for now.
|
||||
# TODO: write tests for the commands.
|
||||
|
||||
def list_command(args):
|
||||
if args.host:
|
||||
for name, path in Host(args.host):
|
||||
print(name + ' [' + path + ']')
|
||||
else:
|
||||
for host in Host.all():
|
||||
print(host.name)
|
||||
for name, path in host:
|
||||
print(' ' + name + ' [' + path + ']')
|
||||
|
||||
|
||||
def show_command(args):
|
||||
path = Session(Host(args.host), args.name).path
|
||||
if not os.path.exists(path):
|
||||
sys.stderr.write('Session "%s" does not exist [%s].\n'
|
||||
% (args.name, path))
|
||||
sys.exit(1)
|
||||
|
||||
with codecs.open(path, encoding='utf8') as f:
|
||||
print(path + ':\n')
|
||||
proc = PygmentsProcessor()
|
||||
print(proc.process_body(f.read(), 'application/json', 'json'))
|
||||
print('')
|
||||
|
||||
|
||||
def delete_command(args):
|
||||
host = Host(args.host)
|
||||
if not args.name:
|
||||
host.delete()
|
||||
else:
|
||||
Session(host, args.name).delete()
|
||||
|
||||
|
||||
def edit_command(args):
|
||||
editor = os.environ.get('EDITOR', None)
|
||||
if not editor:
|
||||
sys.stderr.write(
|
||||
'You need to configure the environment variable EDITOR.\n')
|
||||
sys.exit(1)
|
||||
command = editor.split()
|
||||
command.append(Session(Host(args.host), args.name).path)
|
||||
subprocess.call(command)
|
||||
|
||||
|
||||
def add_commands(subparsers):
|
||||
|
||||
# List
|
||||
list_ = subparsers.add_parser('session-list', help='list sessions')
|
||||
list_.set_defaults(command=list_command)
|
||||
list_.add_argument('host', nargs=OPTIONAL)
|
||||
|
||||
# Show
|
||||
show = subparsers.add_parser('session-show', help='show a session')
|
||||
show.set_defaults(command=show_command)
|
||||
show.add_argument('host')
|
||||
show.add_argument('name')
|
||||
|
||||
# Edit
|
||||
edit = subparsers.add_parser(
|
||||
'session-edit', help='edit a session in $EDITOR')
|
||||
edit.set_defaults(command=edit_command)
|
||||
edit.add_argument('host')
|
||||
edit.add_argument('name')
|
||||
|
||||
# Delete
|
||||
delete = subparsers.add_parser('session-delete', help='delete a session')
|
||||
delete.set_defaults(command=delete_command)
|
||||
delete.add_argument('host')
|
||||
delete.add_argument('name', nargs=OPTIONAL,
|
||||
help='The name of the session to be deleted.'
|
||||
' If not specified, all host sessions are deleted.')
|
15
setup.py
15
setup.py
@ -1,5 +1,7 @@
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import codecs
|
||||
from setuptools import setup
|
||||
import httpie
|
||||
|
||||
@ -22,11 +24,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__,
|
||||
@ -36,6 +47,8 @@ setup(
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'http = httpie.__main__:main',
|
||||
# Not ready yet.
|
||||
# 'httpie = httpie.manage:main',
|
||||
],
|
||||
},
|
||||
install_requires=requirements,
|
||||
|
332
tests/tests.py
332
tests/tests.py
@ -19,20 +19,25 @@ To make it run faster and offline you can::
|
||||
HTTPBIN_URL=http://localhost:5000 tox
|
||||
|
||||
"""
|
||||
from functools import partial
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import tempfile
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
from requests.compat import urlparse
|
||||
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):
|
||||
if cond:
|
||||
@ -40,11 +45,10 @@ except ImportError:
|
||||
return test_method
|
||||
return decorator
|
||||
|
||||
|
||||
from requests import __version__ as requests_version
|
||||
from requests.compat import is_windows, is_py26, bytes, str
|
||||
|
||||
|
||||
|
||||
#################################################################
|
||||
# Utils/setup
|
||||
#################################################################
|
||||
@ -53,7 +57,7 @@ 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 exit
|
||||
from httpie import input
|
||||
from httpie.models import Environment
|
||||
from httpie.core import main
|
||||
@ -61,6 +65,7 @@ from httpie.output import BINARY_SUPPRESSED_NOTICE
|
||||
from httpie.input import ParseError
|
||||
|
||||
|
||||
CRLF = '\r\n'
|
||||
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
||||
'http://httpbin.org').rstrip('/')
|
||||
|
||||
@ -97,11 +102,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,8 +121,36 @@ 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
|
||||
@ -142,18 +180,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 = exit.ERROR
|
||||
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
|
||||
output = env.stdout.read()
|
||||
|
||||
try:
|
||||
r = StrResponse(output.decode('utf8'))
|
||||
except UnicodeDecodeError:
|
||||
@ -166,7 +203,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:
|
||||
@ -502,8 +539,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 +559,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 +568,7 @@ class PrettyFlagTest(BaseTestCase):
|
||||
|
||||
def test_force_ugly(self):
|
||||
r = http(
|
||||
'--ugly',
|
||||
'--pretty=none',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
)
|
||||
@ -544,7 +581,7 @@ class PrettyFlagTest(BaseTestCase):
|
||||
"""
|
||||
r = http(
|
||||
'--print=B',
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
httpbin('/post'),
|
||||
'Content-Type:text/foo+json',
|
||||
'a=b',
|
||||
@ -552,6 +589,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 +753,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 +830,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',
|
||||
@ -800,7 +867,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
httpbin('/status/200')
|
||||
)
|
||||
self.assertIn(OK, r)
|
||||
self.assertEqual(r.exit_status, EXIT.OK)
|
||||
self.assertEqual(r.exit_status, exit.OK)
|
||||
|
||||
def test_error_response_exits_0_without_check_status(self):
|
||||
r = http(
|
||||
@ -808,7 +875,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, exit.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, exit.ERROR_TIMEOUT)
|
||||
|
||||
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
|
||||
r = http(
|
||||
@ -819,19 +895,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, exit.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, exit.OK)
|
||||
|
||||
def test_4xx_check_status_exits_4(self):
|
||||
r = http(
|
||||
@ -840,7 +918,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, exit.ERROR_HTTP_4XX)
|
||||
# Also stderr should be empty since stdout isn't redirected.
|
||||
self.assertTrue(not r.stderr)
|
||||
|
||||
@ -851,34 +929,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, exit.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 +966,7 @@ class StreamTest(BaseTestCase):
|
||||
with open(BIN_FILE_PATH, 'rb') as f:
|
||||
r = http(
|
||||
'--verbose',
|
||||
'--pretty',
|
||||
'--pretty=all',
|
||||
'--stream',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
@ -909,7 +985,7 @@ class StreamTest(BaseTestCase):
|
||||
"""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',
|
||||
@ -927,7 +1003,7 @@ class StreamTest(BaseTestCase):
|
||||
"""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 +1019,68 @@ 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):
|
||||
#noinspection PyUnresolvedReferences
|
||||
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.
|
||||
#################################################################
|
||||
@ -1098,6 +1236,124 @@ class ArgumentParserTestCase(unittest.TestCase):
|
||||
])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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