Compare commits

...

92 Commits
0.2.7 ... 0.4.1

Author SHA1 Message Date
f0c42cd089 v0.4.1 2013-02-26 14:37:09 +01:00
5c6cea79a1 Removed a reference to the removed httpie command
Closes #131
2013-02-26 14:31:52 +01:00
2bed81059a Updated README. 2013-02-22 14:04:27 +01:00
be0b2f21d2 v0.4.0 2013-02-22 13:52:50 +01:00
d97a610f7c Added new logo by @claudiatd 2013-02-22 13:51:37 +01:00
5cc5b13555 Removed the management command.
It means that:

    httpie session list
    httpie session edit
    ...

are gone.

It has never been part of a stable release, and since it wasn't
a very useful feature, it's beeing removed now to avoid feature creep.
2013-02-22 13:27:26 +01:00
3043f24733 .gitignore 2013-02-22 13:19:18 +01:00
093dab5896 Multiple headers TODO. 2013-02-22 13:18:18 +01:00
5f42a21cfb Simplified stored session cookie data. 2013-01-22 20:03:28 +01:00
4c45f0d91f Session name escaping. 2013-01-22 20:02:39 +01:00
d7ec7b2217 Fixing tests for Travis. 2013-01-04 03:19:38 +01:00
7817dfbbcc Fixing tests for Travis. 2013-01-04 03:09:21 +01:00
238b2e0441 Fixing tests for Travis. 2013-01-04 03:05:36 +01:00
a93d57b58b Fixed request/response session cookies.
Closes #113.
2013-01-04 02:59:05 +01:00
79c412064a Python 3.3 fixes. 2013-01-03 15:19:21 +01:00
0ae9d7af58 Compatibility with requests v1.0.4 (requests URL params). 2013-01-03 14:42:17 +01:00
80e317fe24 Added Python 3.3 to tox and travis conf. 2013-01-03 14:14:22 +01:00
1481749c22 Use urlsplit instead of urlparse.
Closes #118.
2013-01-03 14:12:27 +01:00
d84d94dd55 Clean up 2013-01-03 13:49:41 +01:00
1913b0d438 Merge branch 'master' of github.com:jkbr/httpie 2012-12-19 12:31:34 +01:00
fe16f425a9 Require Requests v1.0.3. 2012-12-19 12:31:01 +01:00
7ff71a7f10 Revert: Test Python 3.3 on Travis.
3.3 still not supported
2012-12-19 11:56:02 +01:00
4a37d10245 Test Python 3.3 on Travis. 2012-12-19 11:53:26 +01:00
e5edb66ae8 Requests v1.0: Fixed request body access. 2012-12-19 11:37:52 +01:00
1766dd8291 Requests 1.0: session cookies. 2012-12-17 17:18:18 +01:00
675a8b17ad Merge branch 'master' of github.com:jkbr/httpie 2012-12-17 17:14:24 +01:00
69e26b8bc8 Requests 1.0: prefetch; default_headers. 2012-12-17 17:02:27 +01:00
291f520e0c Update README.rst 2012-12-17 12:26:57 +01:00
9ec328ff6f Session commands. 2012-12-11 12:54:34 +01:00
f2d59ba6bd Improved --check-status + HTTP error + stdout redirect warning. 2012-12-05 05:27:11 +01:00
53caf6ae72 Cleanup 2012-12-05 05:06:06 +01:00
8175366f27 PEP8 2012-12-05 04:39:56 +01:00
8190a7c0c6 Fixed httpie session list 2012-12-05 04:36:42 +01:00
4a615e762f Updated session docs. 2012-12-01 18:43:33 +01:00
7426b4b493 RST formatting. 2012-12-01 18:26:15 +01:00
2cdcadd9d5 Added docs for httpie. 2012-12-01 18:25:34 +01:00
18510a9396 Progress on httpie session *. 2012-12-01 18:16:00 +01:00
acf5f063c7 Typo 2012-12-01 16:52:23 +01:00
2cf379df78 Fixed README typo. 2012-12-01 16:20:16 +01:00
dd100c2cc4 Fixed -j & -v & redirected stdout. Closes #109. 2012-12-01 15:55:58 +01:00
444a9fa929 Added httpless to README. 2012-12-01 15:54:36 +01:00
4a24cd25b9 Clean up. 2012-12-01 15:20:14 +01:00
1c5fb89001 Output stream refactoring. 2012-11-09 15:49:23 +01:00
466e1dbedf Updated CHANGELOG (#100). 2012-11-08 22:39:28 +01:00
d87b2aa0e5 Added support for credentials in URL.
Closes #100 🍰
2012-11-08 22:29:54 +01:00
5d969852c7 Added --no-option's and made args more config-friendly. 2012-09-24 06:49:12 +02:00
bbc702fa11 Improved README. 2012-09-24 05:59:52 +02:00
e25d64a610 0.3.0 2012-09-21 05:50:01 +02:00
a41dd7ac6d Allow output redirection on Windows.
Closes #88.
2012-09-21 04:30:59 +02:00
4a6f32a0f4 Documented config.
Also renamed `default_content_type` to `implicit_content_type` .
2012-09-17 03:08:45 +02:00
548bef7dff Added tests for sessions. 2012-09-17 02:15:00 +02:00
6c2001d1f5 Use the HTTPIE_CONFIG_DIR environment variable. 2012-09-17 02:12:16 +02:00
4029dbf309 Added configuration file.
The "default_content_type" option can be set to "form".

Closes #91.
2012-09-17 00:37:36 +02:00
478d654945 Renamed --session-read to --session-read-only. 2012-09-17 00:01:49 +02:00
66bdbc3745 Cleanup. 2012-09-07 12:48:59 +02:00
316e3f45a9 Added --session-read for read-only sessions. 2012-09-07 12:38:52 +02:00
da0eb7db79 Renamed --allow-redirects to --follow. 2012-09-07 11:58:39 +02:00
9338aadd75 Cleanup 2012-09-05 20:22:08 +02:00
dc7d03e6b8 Merge pull request #90 from simonbuchan/898408c20cfab130699cee3bedbae1ad4a1c69b1
Fix --session for Windows (with a Requests patch)
2012-09-04 02:38:34 -07:00
898408c20c Fix sessions for Windows
':' is invalid in a Windows path, and json needs output to support
write(str).
2012-09-04 15:53:27 +12:00
47de4e2c9c Sessions are now host-bound. 2012-08-19 04:58:14 +02:00
f74424ef03 README 2012-08-18 23:11:56 +02:00
8a9cedb16e JSON session data, `httpie' management command. 2012-08-18 23:07:36 +02:00
ff9f23da5b Grouped arguments for a more user-friendly --help. 2012-08-18 06:12:44 +02:00
50810e5bd9 Include data directory location with --debug. 2012-08-18 04:45:29 +02:00
9b586b953b Use %APPDATA% for data on Windows. 2012-08-18 04:36:58 +02:00
149cbc1604 Fixed Solarized style unavailable on Windows.
#87.
2012-08-18 03:54:38 +02:00
d3df59c8af Updated README. 2012-08-17 23:35:36 +02:00
2057e13a1d Updated README. 2012-08-17 23:35:06 +02:00
4957686bcd Updated README. 2012-08-17 23:34:42 +02:00
4c0d7d526f Added initial support for persistent sessions. 2012-08-17 23:30:47 +02:00
0b3bad9c81 Added initial support for persistent sessions. 2012-08-17 23:23:02 +02:00
1ed43c1a1e Semver-compatible versioning. 2012-08-17 21:24:34 +02:00
bf03937f06 Unified output processing options under --pretty.
* --pretty=none instead of --ugly
* --pretty=all instead of --pretty
* --pretty=colors instead of --colors
* --pretty=format instead of --format
2012-08-17 21:15:37 +02:00
4660da949f Fixed colorized output on Windows with Python 3.
Closes #87.
2012-08-17 06:35:18 +02:00
86256af1df Removed non-ASCII characters from README (closes #85). 2012-08-16 18:47:30 +02:00
8bf7f8219c Fixed readme decoding.
Closes #85.
2012-08-16 03:11:15 +02:00
a5522b8233 Revert "Iter body lines to avoid binary false positives."
This reverts commit b92a3a6d95.
2012-08-16 03:06:48 +02:00
b92a3a6d95 Iter body lines to avoid binary false positives.
#84
2012-08-13 23:33:25 +02:00
9098e5b6e8 Updated changelog. 2012-08-12 06:02:13 +02:00
68640a81b3 Use CRLF for headers in the output. 2012-08-10 01:45:07 +02:00
27f08920c4 Improved examples. 2012-08-09 23:36:29 +02:00
c01dd8d64a Added exit status for timed-out requests. 2012-08-09 05:24:58 +02:00
76feea2f68 Added README reStructuredText validation. 2012-08-07 17:20:50 +02:00
22a10aec4a Added --colors and --format.
Closes #59 and #82.
2012-08-07 16:59:49 +02:00
fa334bdf4d Documented --verify. 2012-08-07 15:25:24 +02:00
f6724452cf Skip tests with redirects on Requests 0.13.6. 2012-08-07 15:08:28 +02:00
07de32c406 Version fix. 2012-08-07 15:01:04 +02:00
1fbe7a6121 Improved --debug. 2012-08-07 14:50:51 +02:00
49e44d9b7e Pre-process README.rst so that PyPi renders it. 2012-08-07 14:50:17 +02:00
193683afbb Added proxy docs. 2012-08-07 14:49:43 +02:00
126b1da515 v0.2.8dev 2012-08-07 00:13:27 +02:00
18 changed files with 1621 additions and 547 deletions

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ build
README.html
.coverage
htmlcov
.idea
.DS_Store

View File

@ -3,7 +3,7 @@ python:
- 2.6
- 2.7
- pypy
- 3.2
- 3.3
script: python setup.py test
install:
- pip install . --use-mirrors

View File

@ -8,6 +8,7 @@ HTTPie authors
Patches and ideas
-----------------
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
* `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_
* `Vladimir Berkutov <https://github.com/dair-targ>`_

View File

@ -1,12 +1,11 @@
***********************
HTTPie: cURL for Humans
***********************
****************************************
HTTPie: a CLI, cURL-like tool for humans
****************************************
v0.2.7
HTTPie is a **command line HTTP client** whose goal is to make CLI interaction
with HTTP-based services as **human-friendly** as possible. It provides a
simple ``http`` command that allows for sending arbitrary HTTP requests with a
HTTPie is a **command line HTTP client**. Its goal is to make CLI interaction
with web services as **human-friendly** as possible. It provides a
simple ``http`` command that allows for sending arbitrary HTTP requests using a
simple and natural syntax, and displays colorized responses. HTTPie can be used
for **testing, debugging**, and generally **interacting** with HTTP servers.
@ -15,7 +14,11 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
:alt: HTTPie compared to cURL
:width: 835
:height: 835
:align: center
.. image:: https://raw.github.com/claudiatd/httpie-artwork/master/images/httpie_logo_simple.png
:alt: HTTPie logo
:align: center
HTTPie is written in Python, and under the hood it uses the excellent
`Requests`_ and `Pygments`_ libraries.
@ -30,7 +33,6 @@ HTTPie is written in Python, and under the hood it uses the excellent
:backlinks: none
=============
Main Features
=============
@ -39,10 +41,11 @@ Main Features
* Formatted and colorized terminal output
* Built-in JSON support
* Forms and file uploads
* HTTPS and authorization
* HTTPS, proxies, and authentication
* Arbitrary request data
* Custom headers
* Python 2.6 and Python 3 support
* Persistent sessions
* Python 2.6, 2.7 and 3.x support
* Linux, Mac OS X and Windows support
* Documentation
* Test coverage
@ -58,9 +61,11 @@ or ``easy_install``:
.. code-block:: bash
$ pip install -U httpie
$ pip install --upgrade httpie
Alternatively:
.. code-block:: bash
$ easy_install httpie
@ -76,16 +81,17 @@ Or, you can install the **development version** directly from GitHub:
.. code-block:: bash
$ pip install -U https://github.com/jkbr/httpie/tarball/master
$ pip install --upgrade https://github.com/jkbr/httpie/tarball/master
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
Linux distributions as well.
Linux distributions as well. However, there may be a significant delay between
official HTTPie releases and package updates.
===========
Quick Start
===========
=====
Usage
=====
Hello World:
@ -111,53 +117,64 @@ Examples
--------
Send a ``HEAD`` request:
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
.. code-block:: bash
$ http HEAD example.org
$ http PUT example.org X-API-Token:123 name=John
Submit a form:
Submitting `forms`_:
.. code-block:: bash
$ http --form POST example.org hello=World
$ http -f POST example.org hello=World
Send a ``PUT`` request with a custom header and some JSON data:
See the request that is being sent using one of the `output options`_:
.. code-block:: bash
$ http PUT example.org X-API-Token:123 name='David Bowie'
See the request that is being sent:
.. code-block:: bash
$ http --verbose example.org
$ http -v example.org
Use `Github API`_ to post a comment on an issue:
Use `Github API`_ to post a comment on an issue with `authentication`_:
.. code-block:: bash
$ http -a USERNAME POST https://api.github.com/repos/jkbr/httpie/issues/83/comments body='HTTPie is awesome!'
Upload a file:
Upload a file using `redirected input`_:
.. code-block:: bash
$ http example.org < file.json
Download a file:
Download a file and save it via `redirected output`_:
.. code-block:: bash
$ http example.org/file > file
Use named `sessions`_ to make certain aspects or the communication persistent
between requests to the same host:
.. code-block:: bash
$ http --session=logged-in -a username:password httpbin.org/get API-Key:123
$ http --session=logged-in httpbin.org/headers
..
--------
*What follows is a detailed documentation. It covers the command syntax,
advanced usage, and also features additional examples.*
============
HTTP Method
============
@ -169,7 +186,7 @@ The name of the HTTP method comes right before the URL argument:
$ http DELETE example.org/todos/7
It makes the command look similar to the actual ``Request-Line`` that is sent:
Which looks similar to the actual ``Request-Line`` that is sent:
.. code-block:: http
@ -177,29 +194,7 @@ It makes the command look similar to the actual ``Request-Line`` that is sent:
When the ``METHOD`` argument is **omitted** from the command, HTTPie defaults to
either ``GET`` or ``POST``. This depends on whether you are sending
some data:
.. code-block:: bash
$ http example.org/todos text='Check out HTTPie'
.. code-block:: http
POST /todos HTTP/1.1
, or no data at all:
.. code-block:: bash
$ http example.org/todos
.. code-block:: http
GET /todos HTTP/1.1
either ``GET`` (with no request data) or ``POST`` (with request data).
===========
@ -266,11 +261,12 @@ their type is distinguished only by the separator used:
+-----------------------+-----------------------------------------------------+
You can use ``\`` to escape characters that shouldn't be used as separators
(or parts thereof). e.g., ``foo\==bar`` will become a data key/value
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
pair (``foo=`` and ``bar``) instead of a URL parameter.
No that data fields aren't the only way to specify request data,
`redirected input`_ allows passing arbitrary data to be sent with the request.
Note that data fields aren't the only way to specify request data:
`Redirected input`_ allows for passing arbitrary data to be sent with the
request.
====
@ -278,7 +274,7 @@ JSON
====
JSON is the *lingua franca* of modern web services and it is also the
**default content type** HTTPie uses:
**implicit content type** HTTPie by default uses:
If your command includes some data items, they are serialized as a JSON
object by default. HTTPie also automatically sets the following headers,
@ -289,9 +285,9 @@ both of which can be overwritten:
``Accept`` ``application/json``
================ =======================================
You can use ``--json`` / ``-j`` to set ``Accept`` to ``application/json``
regardless of whether you are sending data (it's a shortcut for using setting
the header via the usual header notation
You can use ``--json`` / ``-j`` to explicitly set ``Accept``
to ``application/json`` regardless of whether you are sending data
(it's a shortcut for setting the header via the usual header notation
``http url Accept:application/json``).
Simple example:
@ -342,6 +338,12 @@ into the resulting object:
}
Send JSON data stored in a file (see `redirected input`_ for more examples):
.. code-block:: bash
$ http POST api.example.com/person/1 < person.json
=====
Forms
@ -349,9 +351,13 @@ Forms
Submitting forms is very similar to sending `JSON`_ requests. Often the only
difference is in adding the ``--form`` / ``-f`` option, which ensures that
data fields are serialized and ``Content-Type`` is set to
data fields are serialized as, and ``Content-Type`` is set to,
``application/x-www-form-urlencoded; charset=utf-8``.
It is possible to make form data the implicit content type instead of JSON
via the `config`_ file.
-------------
Regular Forms
-------------
@ -374,7 +380,7 @@ Regular Forms
File Upload Forms
-----------------
When one or more file fields are present, the content type is
If one or more file fields is present, the serialization and content type is
``multipart/form-data``:
.. code-block:: bash
@ -416,8 +422,7 @@ To set custom headers you can use the ``Header:Value`` notation:
X-Foo: Bar
There are a couple of default headers that HTTPie sets, but they can easily
be overwritten:
There are a couple of default headers that HTTPie sets:
.. code-block:: http
@ -428,12 +433,15 @@ be overwritten:
Host: <taken-from-URL>
====
Auth
====
Any of the default headers can be overwritten.
The currently supported authorization schemes are Basic and Digest (more to
come). There are two flags that control authorization:
==============
Authentication
==============
The currently supported authentication schemes are Basic and Digest (more to
come). There are two flags that control authentication:
=================== ======================================================
``--auth, -a`` Pass a ``username:password`` pair as
@ -441,12 +449,16 @@ come). There are two flags that control authorization:
(``-a username``), you'll be prompted for
the password before the request is sent.
To send a an empty password, pass ``username:``.
The ``username:password@hostname`` URL syntax is
supported as well (but credentials passed via ``-a``
have higher priority).
``--auth-type`` Specify the auth mechanism. Possible values are
``basic`` and ``digest``. The default value is
``basic`` so it can often be omitted.
=================== ======================================================
Authorization information from ``.netrc`` is honored as well.
Basic auth:
@ -471,6 +483,47 @@ With password prompt:
$ http -a username example.org
=======
Proxies
=======
You can specify proxies to be used through the ``--proxy`` argument:
.. code-block:: bash
$ http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
With Basic authentication:
.. code-block:: bash
$ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
You can also configure proxies by environment variables ``HTTP_PROXY`` and
``HTTPS_PROXY``, and the underlying Requests library will pick them up as well.
If you want to disable proxies configured through the environment variables for
certain hosts, you can specify them in ``NO_PROXY``.
In your ``~/.bash_profile``:
.. code-block:: bash
export HTTP_PROXY=10.10.1.10:3128
export HTTPS_PROXY=10.10.1.10:1080
export NO_PROXY=localhost,example.com
=====
HTTPS
=====
To skip the host's SSL certificate verification, you can pass ``--verify=no``
(default is ``yes``). You can also use ``--verify`` to set a custom CA bundle
path. The path can also be configured via the environment variable
``REQUESTS_CA_BUNDLE``.
==============
Output Options
==============
@ -530,7 +583,7 @@ Character Stands for
``b`` Response body.
========== ==================
Print both the request and response headers:
Print request and response headers:
.. code-block:: bash
@ -547,14 +600,14 @@ request, except that it applies to any HTTP method you use.
Let's say that there is an API that returns the whole resource when it is
updated, but you are only interested in the response headers to see the
status code after the update:
status code after an update:
.. code-block:: bash
$ http --headers PATCH example.org/Really-Huge-Resource name='New Name'
Since we are only printing the HTTP headers here, the connection to server
Since we are only printing the HTTP headers here, the connection to the server
is closed as soon as all the response headers have been received.
Therefore, bandwidth and time isn't wasted downloading the body
which you don't care about.
@ -603,14 +656,14 @@ You can use ``cat`` to enter multiline data on the terminal:
.. code-block:: bash
$ cat | http POST example.com
$ cat | http POST example.com
<paste>
^D
.. code-block:: bash
$ cat | http POST example.com/todos Content-Type:text/plain
$ cat | http POST example.com/todos Content-Type:text/plain
- buy milk
- call parents
^D
@ -635,7 +688,7 @@ Body Data From a Filename
``@/path/to/file``) whose content is used as if it came from ``stdin``.
It has the advantage that **the** ``Content-Type``
**header will automatically be set** to the appropriate value based on the
**header is automatically set** to the appropriate value based on the
filename extension. For example, the following request sends the
verbatim contents of that XML file with ``Content-Type: application/xml``:
@ -648,7 +701,8 @@ verbatim contents of that XML file with ``Content-Type: application/xml``:
Terminal Output
=================
HTTPie does several things by default to make its terminal output easy to read.
HTTPie does several things by default in order to make its terminal output
easy to read.
---------------------
@ -656,30 +710,42 @@ Colors and Formatting
---------------------
Syntax highlighting is applied to HTTP headers and bodies (where it makes
sense). Also, the following formatting is used:
sense). You can choose your prefered color scheme via the ``--style`` option
if you don't like the default one (see ``$ http --help`` for the possible
values).
Also, the following formatting is applied:
* HTTP headers are sorted by name.
* JSON data is indented, sorted by keys, and unicode escapes are converted
to the characters they represent.
Colorizing and formatting can be disabled with ``--ugly, -u``.
One of these options can be used to control output processing:
==================== ========================================================
``--pretty=all`` Apply both colors and formatting.
Default for terminal output.
``--pretty=colors`` Apply colors.
``--pretty=format`` Apply formatting.
``--pretty=none`` Disables output processing.
Default for redirected output.
==================== ========================================================
-----------
Binary data
-----------
Binary data is suppressed for terminal output, which makes it safe to perform
requests to URLs send back binary data. Binary data is suppressed also in
requests to URLs that send back binary data. Binary data is suppressed also in
redirected, but prettified output. The connection is closed as soon as we know
that the response body is binary,
.. code-block:: bash
http example.org/File.mov
$ http example.org/Movie.mov
You will immediately see something like this:
You will nearly instantly see something like this:
.. code-block:: http
@ -701,7 +767,7 @@ Redirected Output
HTTPie uses **different defaults** for redirected output than for
`terminal output`_:
* Formatting and colors aren't applied (unless ``--pretty`` is set).
* Formatting and colors aren't applied (unless ``--pretty`` is specified).
* Only the response body is printed (unless one of the `output options`_ is set).
* Also, binary data isn't suppressed.
@ -723,12 +789,26 @@ Download an image of Octocat, resize it using ImageMagick, upload it elsewhere:
$ http octodex.github.com/images/original.jpg | convert - -resize 25% - | http example.org/Octocats
Force colorizing and formatting, and show both the request and response in
Force colorizing and formatting, and show both the request and the response in
``less`` pager:
.. code-block:: bash
$ http --pretty --verbose example.org | less -R
$ http --pretty=all --verbose example.org | less -R
The ``-R`` flag tells ``less`` to interpret color escape sequences included
HTTPie`s output.
You can create a shortcut for invoking HTTPie with colorized and paged output
by adding the following to your ``~/.bash_profile``:
.. code-block:: bash
function httpless {
# `httpless example.org'
http --pretty=all "$@" | less -R;
}
==================
@ -737,7 +817,7 @@ Streamed Responses
Responses are downloaded and printed in chunks, which allows for streaming
and large file downloads without using too much RAM. However, when
`colors and formatting`_ are applied, the whole response is buffered and only
`colors and formatting`_ is applied, the whole response is buffered and only
then processed at once.
@ -748,7 +828,7 @@ You can use the ``--stream, -S`` flag to make two things happen:
2. Streaming becomes enabled even when the output is prettified: It will be
applied to **each line** of the response and flushed immediately. This makes
it possible to have a nice output of long-lived requests, such as one
it possible to have a nice output for long-lived requests, such as one
to the Twitter streaming API.
@ -759,7 +839,7 @@ Prettified streamed response:
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track='Justin Bieber'
Streamed output by small chunks:
Streamed output by small chunks alá ``tail -f``:
.. code-block:: bash
@ -768,6 +848,94 @@ Streamed output by small chunks:
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track=Apple \
| while read tweet; do echo "$tweet" | http POST example.org/tweets ; done
========
Sessions
========
By default, every request is completely independent of the previous ones.
HTTPie also supports persistent sessions, where custom headers, authorization,
and cookies (manually specified or sent by the server) persist between
requests to the same host.
Create a new session named ``user1``:
.. code-block:: bash
$ http --session=user1 -a user1:password example.org X-Foo:Bar
Now you can refer to the session by its name, and the previously used
authorization and HTTP headers will automatically be set:
.. code-block:: bash
$ http --session=user1 example.org
To create or reuse a different session, simple specify a different name:
.. code-block:: bash
$ http --session=user2 -a user2:password example.org X-Bar:Foo
To use a session without updating it from the request/response exchange
once it is created, specify the session name via
``--session-read-only=SESSION_NAME`` instead.
Session data are stored in JSON files in the directory
``~/.httpie/sessions/<host>/<name>.json``
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
**Warning:** All session data, including credentials, cookie data,
and custom headers are stored in plain text.
Session files can also be created or edited with a text editor.
.. code-block:: bash
$ httpie session edit example.org user1
See also `Config`_.
======
Config
======
HTTPie uses a simple configuration file that contains a JSON object with the
following keys:
========================= =================================================
``__meta__`` HTTPie automatically stores some metadata here.
Do not change.
``implicit_content_type`` A ``String`` specifying the implicit content type
for request data. The default value for this
option is ``json`` and can be changed to
``form``.
``default_options`` An ``Array`` (by default empty) of options
that should be applied to every request.
For instance, you can use this option to change
the default style and output options:
``"default_options": ["--style=fruity", "--body"]``
Another useful default option is
``"--session=default"`` to make HTTPie always
use `sessions`_.
Default options from config file can be unset
for a particular invocation via
``--no-OPTION`` arguments passed on the
command line (e.g., ``--no-style``
or ``--no-session``).
========================= =================================================
The default location of the configuration file is ``~/.httpie/config.json``
(or ``%APPDATA%\httpie\config.json`` on Windows).
The config directory location can be changed by setting the
``HTTPIE_CONFIG_DIR`` environment variable.
=========
Scripting
@ -776,17 +944,19 @@ Scripting
When using HTTPie from **shell scripts**, it can be handy to set the
``--check-status`` flag. It instructs HTTPie to exit with an error if the
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``,
respectively:
be ``3`` (unless ``--follow`` is set), ``4``, or ``5``,
respectively. Also, the ``--timeout`` option allows to overwrite the default
30s timeout:
.. code-block:: bash
#!/bin/bash
if http --check-status HEAD example.org/health &> /dev/null; then
if http --timeout=2.5 --check-status HEAD example.org/health &> /dev/null; then
echo 'OK!'
else
case $? in
2) echo 'Request timed out!' ;;
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
4) echo 'HTTP 4xx Client Error!' ;;
5) echo 'HTTP 5xx Server Error!' ;;
@ -831,10 +1001,15 @@ and that only a small portion of the command is used to control HTTPie and
doesn't directly correspond to any part of the request (here it's only ``-f``
asking HTTPie to send a form request).
The two modes, ``--pretty, -p`` (default for terminal) and ``--ugly, -u``
The two modes, ``--pretty=all`` (default for terminal) and ``--pretty=none``
(default for redirected output), allow for both user-friendly interactive use
and usage from scripts, where HTTPie serves as a generic HTTP client.
As HTTPie is still under heavy development, the existing command line
syntax and some of the ``--OPTIONS`` may change slightly before
HTTPie reaches its final version ``1.0``. All changes are recorded in the
`changelog`_.
==========
Contribute
@ -869,7 +1044,7 @@ Please run the existing suite of tests before a pull request is submitted:
`Tox`_ can also be used to conveniently run tests in all of the
`supported Python environments`_:
.. code-b®lock:: bash
.. code-block:: bash
# Install tox
pip install tox
@ -878,8 +1053,13 @@ Please run the existing suite of tests before a pull request is submitted:
tox
Don't forget to add yourself to `AUTHORS`_.
Don't forget to add yourself to `AUTHORS.rst`_.
=======
Logo
=======
See `claudiatd/httpie-artwork`_
=======
Authors
@ -888,7 +1068,6 @@ Authors
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
have contributed.
=======
Licence
=======
@ -900,14 +1079,37 @@ Please see `LICENSE`_.
Changelog
=========
* `0.2.8dev`_
*You can click a version name to see a diff with the previous one.*
* `0.4.1`_ (2013-02-26)
* Fixed ``setup.py``.
* `0.4.0`_ (2013-02-22)
* Python 3.3 compatibility.
* Requests >= v1.0.4 compatibility.
* Added support for credentials in URL.
* Added ``--no-option`` for every ``--option`` to be config-friendly.
* Mutually exclusive arguments can be specified multiple times. The
last value is used.
* `0.3.0`_ (2012-09-21)
* Allow output redirection on Windows.
* Added configuration file.
* Added persistent session support.
* Renamed ``--allow-redirects`` to ``--follow``.
* Improved the usability of ``http --help``.
* Fixed installation on Windows with Python 3.
* Fixed colorized output on Windows with Python 3.
* CRLF HTTP header field separation in the output.
* Added exit status code ``2`` for timed-out requests.
* Added the option to separate colorizing and formatting
(``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``).
``--ugly`` has bee removed in favor of ``--pretty=none``.
* `0.2.7`_ (2012-08-07)
* Compatibility with Requests 0.13.6.
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
streaming also with ``--pretty`` and to ensure a more frequent output
flushing.
* Support for efficient large file downloads.
* Sort headers by name (unless ``--ugly``).
* Sort headers by name (unless ``--pretty=none``).
* Response body is fetched only when needed (e.g., not with ``--headers``).
* Improved content type matching.
* Updated Solarized color scheme.
@ -973,6 +1175,7 @@ Changelog
.. _Jakub Roztocil: http://roztocil.name
.. _@jakubroztocil: https://twitter.com/jakubroztocil
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
.. _0.2.0: https://github.com/jkbr/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jkbr/httpie/compare/0.2.0...0.2.1
@ -980,7 +1183,10 @@ Changelog
.. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5
.. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
.. _0.2.8dev: https://github.com/jkbr/httpie/compare/0.2.7...master
.. _README for stable version: https://github.com/jkbr/httpie/tree/0.2.6#readme
.. _AUTHORS: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
.. _0.3.0: https://github.com/jkbr/httpie/compare/0.2.7...0.3.0
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
.. _0.5.0-alpha: https://github.com/jkbr/httpie/compare/0.4.0...master
.. _stable version: https://github.com/jkbr/httpie/tree/0.3.0#readme
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE

View File

@ -1,16 +1,19 @@
"""
HTTPie - cURL for humans.
HTTPie - a CLI, cURL-like tool for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.2.7'
__version__ = '0.4.1'
__licence__ = 'BSD'
class EXIT:
class ExitStatus:
"""Exit status code constants."""
OK = 0
ERROR = 1
# Used only when requested:
ERROR_TIMEOUT = 2
# Used only when requested with --check-status:
ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5

View File

@ -1,20 +1,22 @@
"""CLI arguments definition.
NOTE: the CLI interface may change before reaching v1.0.
TODO: make the options config friendly, i.e., no mutually exclusive groups to
allow options overwriting.
"""
import argparse
from requests.compat import is_windows
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
from . import __doc__
from . import __version__
from .compat import is_windows
from .sessions import DEFAULT_SESSIONS_DIR, Session
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
PRETTIFY_STDOUT_TTY_ONLY,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS)
OUT_RESP_BODY, OUTPUT_OPTIONS,
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
def _(text):
@ -22,15 +24,72 @@ def _(text):
return ' '.join(text.strip().split())
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip())
parser.add_argument('--version', action='version', version=__version__)
parser = Parser(
description='%s <http://httpie.org>' % __doc__.strip(),
epilog='For every --option there is a --no-option'
' that reverts the option to its default value.\n\n'
'Suggestions and bug reports are greatly appreciated:\n'
'https://github.com/jkbr/httpie/issues'
)
###############################################################################
# Positional arguments.
###############################################################################
positional = parser.add_argument_group(
title='Positional arguments',
description=_('''
These arguments come after any flags and in the
order they are listed here. Only URL is required.
''')
)
positional.add_argument(
'method', metavar='METHOD',
nargs=OPTIONAL,
default=None,
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie
will guess the HTTP method. If there is some
data to be sent, then it will be POST, otherwise GET.
''')
)
positional.add_argument(
'url', metavar='URL',
help=_('''
The protocol defaults to http:// if the
URL does not include one.
''')
)
positional.add_argument(
'items', metavar='REQUEST ITEM',
nargs=ZERO_OR_MORE,
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=_('''
A key-value pair whose type is defined by the
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.
''')
)
###############################################################################
# Content type.
#############################################
###############################################################################
group_type = parser.add_mutually_exclusive_group(required=False)
group_type.add_argument(
content_type = parser.add_argument_group(
title='Predefined content types',
description=None
)
content_type.add_argument(
'--json', '-j', action='store_true',
help=_('''
(default) Data items from the command
@ -39,55 +98,66 @@ group_type.add_argument(
are set to application/json (if not specified).
''')
)
group_type.add_argument(
content_type.add_argument(
'--form', '-f', action='store_true',
help=_('''
Data items from the command line are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded
(if not specified).
The presence of any file fields results
into a multipart/form-data request.
in a multipart/form-data request.
''')
)
# Output options.
#############################################
###############################################################################
# Output processing
###############################################################################
output_processing = parser.add_argument_group(title='Output processing')
parser.add_argument(
'--output', '-o', type=argparse.FileType('w+b'),
output_processing.add_argument(
'--output', '-o', type=FileType('w+b'),
metavar='FILE',
help= argparse.SUPPRESS if not is_windows else _(
help=SUPPRESS if not is_windows else _(
'''
Save output to FILE.
This option is a replacement for piping output to FILE,
which would on Windows result into corrupted data
which would on Windows result in corrupted data
being saved.
'''
)
)
prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument(
'--pretty', dest='prettify', action='store_true',
default=PRETTIFY_STDOUT_TTY_ONLY,
output_processing.add_argument(
'--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()),
help=_('''
If stdout is a terminal, the response is prettified
by default (colorized and indented if it is JSON).
This flag ensures prettifying even when stdout is redirected.
Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors
and formatting
(default for terminal output), "colors", or "format".
''')
)
prettify.add_argument(
'--ugly', '-u', dest='prettify', action='store_false',
output_processing.add_argument(
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
choices=AVAILABLE_STYLES,
help=_('''
Do not prettify the response.
''')
Output coloring style. One of %s. Defaults to "%s".
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
)
output_options = parser.add_mutually_exclusive_group(required=False)
output_options.add_argument('--print', '-p', dest='output_options',
###############################################################################
# Output options
###############################################################################
output_options = parser.add_argument_group(title='Output options')
output_options.add_argument(
'--print', '-p', dest='output_options', metavar='WHAT',
help=_('''
String specifying what the output should contain:
"{request_headers}" stands for the request headers, and
@ -98,12 +168,10 @@ output_options.add_argument('--print', '-p', dest='output_options',
headers and body is printed), if standard output is not redirected.
If the output is piped to another program or to a file,
then only the body is printed by default.
'''.format(
request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,
))
'''.format(request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,))
)
output_options.add_argument(
'--verbose', '-v', dest='output_options',
@ -130,19 +198,9 @@ output_options.add_argument(
'''.format(OUT_RESP_BODY))
)
parser.add_argument(
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
choices=AVAILABLE_STYLES,
output_options.add_argument(
'--stream', '-S', action='store_true', default=False,
help=_('''
Output coloring style, one of %s. Defaults to "%s".
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
)
parser.add_argument('--stream', '-S', action='store_true', default=False, help=_(
'''
Always stream the output by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied),
@ -154,9 +212,100 @@ parser.add_argument('--stream', '-S', action='store_true', default=False, help=_
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
'''
))
parser.add_argument(
''')
)
###############################################################################
# Sessions
###############################################################################
sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False)
sessions.add_argument(
'--session', metavar='SESSION_NAME', type=RegexValidator(
Session.VALID_NAME_PATTERN,
'Session name contains invalid characters.'
),
help=_('''
Create, or reuse and update a session.
Within a session, custom headers, auth credential, as well as any
cookies sent by the server persist between requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR)
)
sessions.add_argument(
'--session-read-only', metavar='SESSION_NAME',
help=_('''
Create or read a session without updating it form the
request/response exchange.
''')
)
###############################################################################
# Authentication
###############################################################################
# ``requests.request`` keyword arguments.
auth = parser.add_argument_group(title='Authentication')
auth.add_argument(
'--auth', '-a', metavar='USER[:PASS]',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_('''
If only the username is provided (-a username),
HTTPie will prompt for the password.
'''),
)
auth.add_argument(
'--auth-type', choices=['basic', 'digest'], default='basic',
help=_('''
The authentication mechanism to be used.
Defaults to "basic".
''')
)
# Network
#############################################
network = parser.add_argument_group(title='Network')
network.add_argument(
'--proxy', default=[], action='append', metavar='PROTOCOL:HOST',
type=KeyValueArgType(SEP_PROXY),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128). You can specify multiple
proxies with different protocols.
''')
)
network.add_argument(
'--follow', default=False, action='store_true',
help=_('''
Set this flag if full redirects are allowed
(e.g. re-POST-ing of data at new ``Location``)
''')
)
network.add_argument(
'--verify', default='yes',
help=_('''
Set to "no" to skip checking the host\'s SSL certificate.
You can also pass the path to a CA_BUNDLE
file for private certs. You can also set
the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes".
''')
)
network.add_argument(
'--timeout', type=float, default=30, metavar='SECONDS',
help=_('''
The connection timeout of the request in seconds.
The default value is 30 seconds.
''')
)
network.add_argument(
'--check-status', default=False, action='store_true',
help=_('''
By default, HTTPie exits with 0 when no network or other fatal
@ -167,7 +316,7 @@ parser.add_argument(
When the server replies with a 4xx (Client Error) or 5xx
(Server Error) status code, HTTPie exits with 4 or 5 respectively.
If the response is a 3xx (Redirect) and --allow-redirects
If the response is a 3xx (Redirect) and --follow
hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected.
@ -175,100 +324,29 @@ parser.add_argument(
''')
)
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_('''
username:password.
If only the username is provided (-a username),
HTTPie will prompt for the password.
'''),
)
parser.add_argument(
'--auth-type', choices=['basic', 'digest'], default='basic',
help=_('''
The authentication mechanism to be used.
Defaults to "basic".
''')
)
###############################################################################
# Troubleshooting
###############################################################################
parser.add_argument(
'--verify', default='yes',
help=_('''
Set to "no" to skip checking the host\'s SSL certificate.
You can also pass the path to a CA_BUNDLE
file for private certs. You can also set
the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes".
''')
troubleshooting = parser.add_argument_group(title='Troubleshooting')
troubleshooting.add_argument(
'--help',
action='help', default=SUPPRESS,
help='Show this help message and exit'
)
parser.add_argument(
'--proxy', default=[], action='append',
type=KeyValueArgType(SEP_PROXY),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
''')
troubleshooting.add_argument(
'--version', action='version', version=__version__)
troubleshooting.add_argument(
'--traceback', action='store_true', default=False,
help='Prints exception traceback should one occur.'
)
parser.add_argument(
'--allow-redirects', default=False, action='store_true',
help=_('''
Set this flag if full redirects are allowed
(e.g. re-POST-ing of data at new ``Location``)
''')
)
parser.add_argument(
'--timeout', type=float,
help=_('''
Float describes the timeout of the request
(Use socket.setdefaulttimeout() as fallback).
''')
)
parser.add_argument(
troubleshooting.add_argument(
'--debug', action='store_true', default=False,
help=_('''
Prints exception traceback should one occur and other
information useful for debugging HTTPie itself.
''')
)
# Positional arguments.
#############################################
parser.add_argument(
'method', metavar='METHOD',
nargs='?',
default=None,
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie
will guess the HTTP method. If there is some
data to be sent, then it will be POST, otherwise GET.
''')
)
parser.add_argument(
'url', metavar='URL',
help=_('''
The protocol defaults to http:// if the
URL does not include one.
''')
)
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=_('''
A key-value pair whose type is defined by the
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.
Prints exception traceback should one occur, and also other
information that is useful for debugging HTTPie itself and
for bug reports.
''')
)

94
httpie/client.py Normal file
View File

@ -0,0 +1,94 @@
import json
import sys
from pprint import pformat
import requests
import requests.auth
from . import sessions
from . import __version__
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
DEFAULT_UA = 'HTTPie/%s' % __version__
def get_response(args, config_dir):
"""Send the request and return a `request.Response`."""
requests_kwargs = get_requests_kwargs(args)
if args.debug:
sys.stderr.write('\n>>> requests.request(%s)\n\n'
% pformat(requests_kwargs))
if not args.session and not args.session_read_only:
response = requests.request(**requests_kwargs)
else:
response = sessions.get_response(
config_dir=config_dir,
name=args.session or args.session_read_only,
request_kwargs=requests_kwargs,
read_only=bool(args.session_read_only),
)
return response
def get_requests_kwargs(args):
"""Translate our `args` into `requests.request` keyword arguments."""
implicit_headers = {
'User-Agent': DEFAULT_UA
}
auto_json = args.data and not args.form
if args.json or auto_json:
implicit_headers['Accept'] = 'application/json'
if args.data:
implicit_headers['Content-Type'] = JSON
if isinstance(args.data, dict):
if args.data:
args.data = json.dumps(args.data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
args.data = ''
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
implicit_headers['Content-Type'] = FORM
for name, value in implicit_headers.items():
if name not in args.headers:
args.headers[name] = value
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'stream': True,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
'data': args.data,
'verify': {
'yes': True,
'no': False
}.get(args.verify, args.verify),
'timeout': args.timeout,
'auth': credentials,
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.follow,
'params': args.params,
}
return kwargs

18
httpie/compat.py Normal file
View File

@ -0,0 +1,18 @@
"""
Python 2/3 compatibility.
"""
#noinspection PyUnresolvedReferences
from requests.compat import (
is_windows,
bytes,
str,
is_py3,
is_py26,
)
try:
from urllib.parse import urlsplit
except ImportError:
from urlparse import urlsplit

95
httpie/config.py Normal file
View File

@ -0,0 +1,95 @@
import os
import json
import errno
from . import __version__
from .compat import is_windows
DEFAULT_CONFIG_DIR = os.environ.get(
'HTTPIE_CONFIG_DIR',
os.path.expanduser('~/.httpie') if not is_windows else
os.path.expandvars(r'%APPDATA%\\httpie')
)
class BaseConfigDict(dict):
name = None
help = None
about = None
directory = DEFAULT_CONFIG_DIR
def __init__(self, directory=None, *args, **kwargs):
super(BaseConfigDict, self).__init__(*args, **kwargs)
if directory:
self.directory = directory
def __getattr__(self, item):
return self[item]
@property
def path(self):
try:
os.makedirs(self.directory, mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return os.path.join(self.directory, self.name + '.json')
@property
def is_new(self):
return not os.path.exists(self.path)
def load(self):
try:
with open(self.path, 'rt') as f:
try:
data = json.load(f)
except ValueError as e:
raise ValueError(
'Invalid %s JSON: %s [%s]' %
(type(self).__name__, e.message, self.path)
)
self.update(data)
except IOError as e:
if e.errno != errno.ENOENT:
raise
def save(self):
self['__meta__'] = {
'httpie': __version__
}
if self.help:
self['__meta__']['help'] = self.help
if self.about:
self['__meta__']['about'] = self.about
with open(self.path, 'w') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write('\n')
def delete(self):
try:
os.unlink(self.path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
class Config(BaseConfigDict):
name = 'config'
help = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file'
DEFAULTS = {
'implicit_content_type': 'json',
'default_options': []
}
def __init__(self, *args, **kwargs):
super(Config, self).__init__(*args, **kwargs)
self.update(self.DEFAULTS)

View File

@ -11,134 +11,119 @@ Invocation flow:
"""
import sys
import json
import errno
import requests
import requests.auth
from requests.compat import str
from httpie import __version__ as httpie_version
from requests import __version__ as requests_version
from pygments import __version__ as pygments_version
from .cli import parser
from .compat import str, is_py3
from .client import get_response
from .models import Environment
from .output import output_stream, write
from . import EXIT
from .output import build_output_stream, write, write_with_colors_win_p3k
from . import ExitStatus
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
def get_response(args):
"""Send the request and return a `request.Response`."""
auto_json = args.data and not args.form
if args.json or auto_json:
if 'Content-Type' not in args.headers and args.data:
args.headers['Content-Type'] = JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = FORM
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
return requests.request(
prefetch=False,
method=args.method.lower(),
url=args.url,
headers=args.headers,
data=args.data,
verify={'yes': True, 'no': False}.get(args.verify, args.verify),
timeout=args.timeout,
auth=credentials,
proxies=dict((p.key, p.value) for p in args.proxy),
files=args.files,
allow_redirects=args.allow_redirects,
params=args.params,
)
def get_exist_status(code, allow_redirects=False):
"""Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not allow_redirects:
def get_exit_status(http_status, follow=False):
"""Translate HTTP status code to exit status code."""
if 300 <= http_status <= 399 and not follow:
# Redirect
return EXIT.ERROR_HTTP_3XX
elif 400 <= code <= 499:
return ExitStatus.ERROR_HTTP_3XX
elif 400 <= http_status <= 499:
# Client Error
return EXIT.ERROR_HTTP_4XX
elif 500 <= code <= 599:
return ExitStatus.ERROR_HTTP_4XX
elif 500 <= http_status <= 599:
# Server Error
return EXIT.ERROR_HTTP_5XX
return ExitStatus.ERROR_HTTP_5XX
else:
return EXIT.OK
return ExitStatus.OK
def print_debug_info(env):
sys.stderr.writelines([
'HTTPie %s\n' % httpie_version,
'HTTPie data: %s\n' % env.config.directory,
'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version,
'Python %s %s\n' % (sys.version, sys.platform)
])
def main(args=sys.argv[1:], env=Environment()):
"""Run the main program and write the output to ``env.stdout``.
Return exit status.
Return exit status code.
"""
if env.config.default_options:
args = env.config.default_options + args
def error(msg, *args):
def error(msg, *args, **kwargs):
msg = msg % args
env.stderr.write('\nhttp: error: %s\n' % msg)
level = kwargs.get('level', 'error')
env.stderr.write('http: %s: %s\n' % (level, msg))
debug = '--debug' in args
status = EXIT.OK
traceback = debug or '--traceback' in args
exit_status = ExitStatus.OK
if debug:
print_debug_info(env)
if args == ['--debug']:
return exit_status
try:
args = parser.parse_args(args=args, env=env)
response = get_response(args)
response = get_response(args, config_dir=env.config.directory)
if args.check_status:
status = get_exist_status(response.status_code,
args.allow_redirects)
if status and not env.stdout_isatty:
error('%s %s', response.raw.status, response.raw.reason)
exit_status = get_exit_status(response.status_code, args.follow)
stream = output_stream(args, env, response.request, response)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
error('HTTP %s %s',
response.raw.status,
response.raw.reason,
level='warning')
write_kwargs = {
'stream': build_output_stream(args, env,
response.request,
response),
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
write(stream=stream,
outfile=env.stdout,
flush=env.stdout_isatty or args.stream)
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_with_colors_win_p3k(**write_kwargs)
else:
write(**write_kwargs)
except IOError as e:
if not debug and e.errno == errno.EPIPE:
# Ignore broken pipes unless --debug.
if not traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
except (KeyboardInterrupt, SystemExit):
if debug:
if traceback:
raise
env.stderr.write('\n')
status = EXIT.ERROR
exit_status = ExitStatus.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
error('Request timed out (%ss).', args.timeout)
except Exception as e:
# TODO: distinguish between expected and unexpected errors.
# network errors vs. bugs, etc.
if debug:
# TODO: Better distinction between expected and unexpected errors.
# Network errors vs. bugs, etc.
if traceback:
raise
error('%s: %s', type(e).__name__, str(e))
status = EXIT.ERROR
exit_status = ExitStatus.ERROR
return status
return exit_status

View File

@ -5,20 +5,21 @@ import os
import sys
import re
import json
import argparse
import mimetypes
import getpass
from io import BytesIO
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
# TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jkbr/httpie/issues/130
from requests.structures import CaseInsensitiveDict
from requests.compat import str, urlparse
from . import __version__
from .compat import urlsplit, str
HTTP_POST = 'POST'
@ -66,15 +67,22 @@ OUTPUT_OPTIONS = frozenset([
OUT_RESP_BODY
])
# Pretty
PRETTY_MAP = {
'all': ['format', 'colors'],
'colors': ['colors'],
'format': ['format'],
'none': []
}
PRETTY_STDOUT_TTY_ONLY = object()
# Defaults
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class Parser(argparse.ArgumentParser):
class Parser(ArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`.
Handles all input (CLI args, file args, stdin), applies defaults,
@ -85,27 +93,29 @@ class Parser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
kwargs['add_help'] = False
super(Parser, self).__init__(*args, **kwargs)
# Help only as --help (-h is used for --headers).
self.add_argument('--help',
action='help', default=argparse.SUPPRESS,
help=argparse._('show this help message and exit'))
#noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None):
self.env = env
if env.is_windows and not env.stdout_isatty:
self.error('Output redirection is not supported on Windows.'
' Please use `--output FILE\' instead.')
args, no_options = super(Parser, self).parse_known_args(args,
namespace)
args = super(Parser, self).parse_args(args, namespace)
self._apply_no_options(args, no_options)
if not args.json and env.config.implicit_content_type == 'form':
args.form = True
if args.debug:
args.traceback = True
if args.output:
env.stdout = args.output
env.stdout_isatty = False
self._process_output_options(args, env)
self._process_pretty_options(args, env)
self._guess_method(args, env)
self._parse_items(args)
@ -116,17 +126,54 @@ class Parser(argparse.ArgumentParser):
scheme = HTTPS if env.progname == 'https' else HTTP
args.url = scheme + args.url
if args.auth and not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(urlparse(args.url).netloc)
if args.prettify == PRETTIFY_STDOUT_TTY_ONLY:
args.prettify = env.stdout_isatty
elif args.prettify and env.is_windows:
self.error('Only terminal output can be prettified on Windows.')
self._process_auth(args)
return args
def _process_auth(self, args):
url = urlsplit(args.url)
if args.auth:
if not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(url.netloc)
elif url.username is not None:
# Handle http://username:password@hostname/
username, password = url.username, url.password
args.auth = AuthCredentials(
key=username,
value=password,
sep=SEP_CREDENTIALS,
orig=SEP_CREDENTIALS.join([username, password])
)
def _apply_no_options(self, args, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g.,
specified in config.
"""
invalid = []
for option in no_options:
if not option.startswith('--no-'):
invalid.append(option)
continue
# --no-option => --option
inverted = '--' + option[5:]
for action in self._actions:
if inverted in action.option_strings:
setattr(args, action.dest, action.default)
break
else:
invalid.append(option)
if invalid:
msg = 'unrecognized arguments: %s'
self.error(msg % ' '.join(invalid))
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
@ -170,8 +217,8 @@ class Parser(argparse.ArgumentParser):
args.items.insert(
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
except argparse.ArgumentTypeError as e:
if args.debug:
except ArgumentTypeError as e:
if args.traceback:
raise
self.error(e.message)
@ -189,7 +236,6 @@ class Parser(argparse.ArgumentParser):
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = ParamDict() if args.form else OrderedDict()
args.files = OrderedDict()
args.params = ParamDict()
@ -201,7 +247,7 @@ class Parser(argparse.ArgumentParser):
files=args.files,
params=args.params)
except ParseError as e:
if args.debug:
if args.traceback:
raise
self.error(e.message)
@ -238,6 +284,14 @@ class Parser(argparse.ArgumentParser):
if unknown:
self.error('Unknown output options: %s' % ','.join(unknown))
def _process_pretty_options(self, args, env):
if args.prettify == PRETTY_STDOUT_TTY_ONLY:
args.prettify = PRETTY_MAP['all' if env.stdout_isatty else 'none']
elif args.prettify and env.is_windows:
self.error('Only terminal output can be colorized on Windows.')
else:
args.prettify = PRETTY_MAP[args.prettify]
class ParseError(Exception):
pass
@ -256,6 +310,38 @@ class KeyValue(object):
return self.__dict__ == other.__dict__
def session_name_arg_type(name):
from .sessions import Session
if not Session.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in session names: "%s"'
% name)
return name
def host_name_arg_type(name):
from .sessions import Host
if not Host.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in host names: "%s"'
% name)
return name
class RegexValidator(object):
def __init__(self, pattern, error_message):
self.pattern = re.compile(pattern)
self.error_message = error_message
def __call__(self, value):
if not self.pattern.search(value):
raise ArgumentError(None, self.error_message)
return value
class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`.
@ -336,7 +422,7 @@ class KeyValueArgType(object):
break
else:
raise argparse.ArgumentTypeError(
raise ArgumentTypeError(
'"%s" is not a valid value' % string)
return self.key_value_class(
@ -375,7 +461,7 @@ class AuthCredentialsArgType(KeyValueArgType):
"""
try:
return super(AuthCredentialsArgType, self).__call__(string)
except argparse.ArgumentTypeError:
except ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
key=string,
@ -397,9 +483,6 @@ class ParamDict(OrderedDict):
data and URL params.
"""
# NOTE: Won't work when used for form data with multiple values
# for a field and a file field is present:
# https://github.com/kennethreitz/requests/issues/737
if key not in self:
super(ParamDict, self).__setitem__(key, value)
else:

View File

@ -1,6 +1,8 @@
import os
import sys
from requests.compat import urlparse, is_windows, bytes, str
from .config import DEFAULT_CONFIG_DIR, Config
from .compat import urlsplit, is_windows, bytes, str
class Environment(object):
@ -11,21 +13,24 @@ class Environment(object):
"""
#noinspection PyUnresolvedReferences
is_windows = is_windows
progname = os.path.basename(sys.argv[0])
if progname not in ['http', 'https']:
progname = 'http'
if is_windows:
import colorama.initialise
colorama.initialise.init()
stdin_isatty = sys.stdin.isatty()
stdin = sys.stdin
stdout_isatty = sys.stdout.isatty()
stdout = sys.stdout
config_dir = DEFAULT_CONFIG_DIR
if stdout_isatty and is_windows:
from colorama.initialise import wrap_stream
stdout = wrap_stream(sys.stdout, convert=None,
strip=None, autoreset=True, wrap=True)
else:
stdout = sys.stdout
stderr = sys.stderr
# Can be set to 0 to disable colors completely.
@ -36,6 +41,16 @@ class Environment(object):
for attr in kwargs.keys())
self.__dict__.update(**kwargs)
@property
def config(self):
if not hasattr(self, '_config'):
self._config = Config(directory=self.config_dir)
if self._config.is_new:
self._config.save()
else:
self._config.load()
return self._config
class HTTPMessage(object):
"""Abstract class for HTTP messages."""
@ -82,19 +97,28 @@ class HTTPResponse(HTTPMessage):
return self._orig.iter_content(chunk_size=chunk_size)
def iter_lines(self, chunk_size):
for line in self._orig.iter_lines(chunk_size):
yield line, b'\n'
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
@property
def headers(self):
original = self._orig.raw._original_response
status_line = 'HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status,
reason=original.reason
)
headers = str(original.msg)
return '\n'.join([status_line, headers]).strip()
version='.'.join(str(original.version)),
status=original.status,
reason=original.reason
)
headers = [status_line]
try:
# `original.msg` is a `http.client.HTTPMessage` on Python 3
# `_headers` is a 2-tuple
headers.extend(
'%s: %s' % header for header in original.msg._headers)
except AttributeError:
# and a `httplib.HTTPMessage` on Python 2.x
# `headers` is a list of `name: val<CRLF>`.
headers.extend(h.strip() for h in original.msg.headers)
return '\r\n'.join(headers)
@property
def encoding(self):
@ -118,40 +142,25 @@ class HTTPRequest(HTTPMessage):
@property
def headers(self):
"""Return Request-Line"""
url = urlparse(self._orig.url)
url = urlsplit(self._orig.url)
# Querystring
qs = ''
if url.query or self._orig.params:
qs = '?'
if url.query:
qs += url.query
# Requests doesn't make params part of ``request.url``.
if self._orig.params:
if url.query:
qs += '&'
#noinspection PyUnresolvedReferences
qs += type(self._orig)._encode_params(self._orig.params)
# Request-Line
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method,
path=url.path or '/',
query=qs
query='?' + url.query if url.query else ''
)
headers = dict(self._orig.headers)
if 'Host' not in headers:
headers['Host'] = urlparse(self._orig.url).netloc
headers['Host'] = url.netloc
headers = ['%s: %s' % (name, value)
for name, value in headers.items()]
headers.insert(0, request_line)
return '\n'.join(headers).strip()
return '\r\n'.join(headers).strip()
@property
def encoding(self):
@ -159,26 +168,8 @@ class HTTPRequest(HTTPMessage):
@property
def body(self):
"""Reconstruct and return the original request body bytes."""
if self._orig.files:
# TODO: would be nice if we didn't need to encode the files again
# FIXME: Also the boundary header doesn't match the one used.
for fn, fd in self._orig.files.values():
# Rewind the files as they have already been read before.
fd.seek(0)
body, _ = self._orig._encode_files(self._orig.files)
else:
try:
body = self._orig.data
except AttributeError:
# requests < 0.12.1
body = self._orig._enc_data
if isinstance(body, dict):
#noinspection PyUnresolvedReferences
body = type(self._orig)._encode_params(body)
if isinstance(body, str):
body = body.encode('utf8')
return body
body = self._orig.body
if isinstance(body, str):
# Happens with JSON/form request data parsed from the command line.
body = body.encode('utf8')
return body or b''

View File

@ -12,21 +12,20 @@ from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound
from requests.compat import is_windows
from .compat import is_windows
from .solarized import Solarized256Style
from .models import HTTPRequest, HTTPResponse, Environment
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY)
# Colors on Windows via colorama aren't that great and fruity
# seems to give the best result there.
# Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there.
AVAILABLE_STYLES = set(STYLE_MAP.keys())
AVAILABLE_STYLES.add('solarized')
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
#noinspection PySetFunctionToLiteral
AVAILABLE_STYLES = set([DEFAULT_STYLE]) | set(STYLE_MAP.keys())
BINARY_SUPPRESSED_NOTICE = (
b'\n'
@ -62,23 +61,38 @@ def write(stream, outfile, flush):
outfile.flush()
def output_stream(args, env, request, response):
def write_with_colors_win_p3k(stream, outfile, flush):
"""Like `write`, but colorized chunks are written as text
directly to `outfile` to ensure it gets processed by colorama.
Applies only to Windows with Python 3 and colorized terminal output.
"""
color = b'\x1b['
encoding = outfile.encoding
for chunk in stream:
if color in chunk:
outfile.write(chunk.decode(encoding))
else:
outfile.buffer.write(chunk)
if flush:
outfile.flush()
def build_output_stream(args, env, request, response):
"""Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks.
"""
Stream = make_stream(env, args)
req_h = OUT_REQ_HEAD in args.output_options
req_b = OUT_REQ_BODY in args.output_options
resp_h = OUT_RESP_HEAD in args.output_options
resp_b = OUT_RESP_BODY in args.output_options
resp_b = OUT_RESP_BODY in args.output_options
req = req_h or req_b
resp = resp_h or resp_b
output = []
Stream = get_stream_type(env, args)
if req:
output.append(Stream(
@ -86,8 +100,9 @@ def output_stream(args, env, request, response):
with_headers=req_h,
with_body=req_b))
if req and resp:
output.append([b'\n\n\n'])
if req_b and resp:
# Request/Response separator.
output.append([b'\n\n'])
if resp:
output.append(Stream(
@ -95,13 +110,15 @@ def output_stream(args, env, request, response):
with_headers=resp_h,
with_body=resp_b))
if env.stdout_isatty:
if env.stdout_isatty and resp_b:
# Ensure a blank line after the response body.
# For terminal output only.
output.append([b'\n\n'])
return chain(*output)
def make_stream(env, args):
def get_stream_type(env, args):
"""Pick the right stream type based on `env` and `args`.
Wrap it in a partial with the type-specific args so that
we don't need to think what stream we are dealing with.
@ -112,12 +129,15 @@ def make_stream(env, args):
RawStream,
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
if args.stream
else RawStream.CHUNK_SIZE)
else RawStream.CHUNK_SIZE
)
elif args.prettify:
Stream = partial(
PrettyStream if args.stream else BufferedPrettyStream,
processor=OutputProcessor(env, pygments_style=args.style),
env=env)
env=env,
processor=OutputProcessor(
env=env, groups=args.prettify, pygments_style=args.style),
)
else:
Stream = partial(EncodedStream, env=env)
@ -125,7 +145,7 @@ def make_stream(env, args):
class BaseStream(object):
"""Base HTTP message stream class."""
"""Base HTTP message output stream class."""
def __init__(self, msg, with_headers=True, with_body=True):
"""
@ -134,37 +154,29 @@ class BaseStream(object):
:param with_body: if `True`, body will be included
"""
assert with_headers or with_body
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
def _headers(self):
def _get_headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('ascii')
def _body(self):
def _iter_body(self):
"""Return an iterator over the message body."""
raise NotImplementedError()
def __iter__(self):
"""Return an iterator over `self.msg`."""
if self.with_headers:
yield self._headers()
yield self._get_headers()
yield b'\r\n\r\n'
if self.with_body:
it = self._body()
try:
if self.with_headers:
# Yield the headers/body separator only if needed.
chunk = next(it)
if chunk:
yield b'\n\n'
yield chunk
for chunk in it:
for chunk in self._iter_body():
yield chunk
except BinarySuppressedError as e:
if self.with_headers:
yield b'\n'
@ -181,7 +193,7 @@ class RawStream(BaseStream):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def _body(self):
def _iter_body(self):
return self.msg.iter_body(self.chunk_size)
@ -194,6 +206,7 @@ class EncodedStream(BaseStream):
"""
CHUNK_SIZE = 1024 * 5
def __init__(self, env=Environment(), **kwargs):
super(EncodedStream, self).__init__(**kwargs)
@ -208,7 +221,7 @@ class EncodedStream(BaseStream):
# Default to utf8 when unsure.
self.output_encoding = output_encoding or 'utf8'
def _body(self):
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
@ -234,11 +247,11 @@ class PrettyStream(EncodedStream):
super(PrettyStream, self).__init__(**kwargs)
self.processor = processor
def _headers(self):
def _get_headers(self):
return self.processor.process_headers(
self.msg.headers).encode(self.output_encoding)
def _body(self):
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
@ -262,9 +275,8 @@ class BufferedPrettyStream(PrettyStream):
CHUNK_SIZE = 1024 * 10
def _body(self):
def _iter_body(self):
#noinspection PyArgumentList
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
body = bytearray()
@ -297,13 +309,13 @@ class HTTPLexer(lexer.RegexLexer):
# Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
lexer.bygroups(
token.Name.Function,
token.Text,
token.Name.Namespace,
token.Text,
token.Keyword.Reserved,
token.Operator,
token.Number
token.Name.Function,
token.Text,
token.Name.Namespace,
token.Text,
token.Keyword.Reserved,
token.Operator,
token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
@ -318,13 +330,14 @@ class HTTPLexer(lexer.RegexLexer):
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
token.Name.Attribute, # Name
token.Name.Attribute, # Name
token.Text,
token.Operator, # Colon
token.Text,
token.String # Value
))
]}
]
}
class BaseProcessor(object):
@ -332,12 +345,11 @@ class BaseProcessor(object):
enabled = True
def __init__(self, env, **kwargs):
def __init__(self, env=Environment(), **kwargs):
"""
:param env:
an class:`Environment` instance
:param kwargs:
additional keyword argument that some processor might require.
:param env: an class:`Environment` instance
:param kwargs: additional keyword argument that some
processor might require.
"""
self.env = env
@ -346,8 +358,7 @@ class BaseProcessor(object):
def process_headers(self, headers):
"""Return processed `headers`
:param headers:
The headers as text.
:param headers: The headers as text.
"""
return headers
@ -355,14 +366,9 @@ class BaseProcessor(object):
def process_body(self, content, content_type, subtype):
"""Return processed `content`.
:param content:
The body content as text
:param content_type:
Full content type, e.g., 'application/atom+xml'.
:param subtype:
E.g. 'xml'.
:param content: The body content as text
:param content_type: Full content type, e.g., 'application/atom+xml'.
:param subtype: E.g. 'xml'.
"""
return content
@ -402,7 +408,8 @@ class PygmentsProcessor(BaseProcessor):
return
try:
style = get_style_by_name(self.kwargs['pygments_style'])
style = get_style_by_name(
self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound:
style = Solarized256Style
@ -440,24 +447,35 @@ class HeadersProcessor(BaseProcessor):
def process_headers(self, headers):
lines = headers.splitlines()
headers = sorted(lines[1:], key=lambda h: h.split(':')[0])
return '\n'.join(lines[:1] + headers)
return '\r\n'.join(lines[:1] + headers)
class OutputProcessor(object):
"""A delegate class that invokes the actual processors."""
installed_processors = [
JSONProcessor,
HeadersProcessor,
PygmentsProcessor
]
def __init__(self, env, **kwargs):
processors = [
cls(env, **kwargs)
for cls in self.installed_processors
installed_processors = {
'format': [
HeadersProcessor,
JSONProcessor
],
'colors': [
PygmentsProcessor
]
self.processors = [p for p in processors if p.enabled]
}
def __init__(self, groups, env=Environment(), **kwargs):
"""
:param env: a :class:`models.Environment` instance
:param groups: the groups of processors to be applied
:param kwargs: additional keyword arguments for processors
"""
self.processors = []
for group in groups:
for cls in self.installed_processors[group]:
processor = cls(env, **kwargs)
if processor.enabled:
self.processors.append(processor)
def process_headers(self, headers):
for processor in self.processors:

179
httpie/sessions.py Normal file
View File

@ -0,0 +1,179 @@
"""Persistent, JSON-serialized sessions.
"""
import re
import os
import glob
import errno
import shutil
import requests
from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from .compat import urlsplit
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
SESSIONS_DIR_NAME = 'sessions'
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
def get_response(name, request_kwargs, config_dir, read_only=False):
"""Like `client.get_response`, but applies permanent
aspects of the session to the request.
"""
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
host = Host(
root_dir=sessions_dir,
name=request_kwargs['headers'].get('Host', None)
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
)
session = Session(host, name)
session.load()
# Update session headers with the request headers.
session['headers'].update(request_kwargs.get('headers', {}))
# Use the merged headers for the request
request_kwargs['headers'] = session['headers']
auth = request_kwargs.get('auth', None)
if auth:
session.auth = auth
elif session.auth:
request_kwargs['auth'] = session.auth
requests_session = requests.Session()
requests_session.cookies = session.cookies
try:
response = requests_session.request(**request_kwargs)
except Exception:
raise
else:
# Existing sessions with `read_only=True` don't get updated.
if session.is_new or not read_only:
session.cookies = requests_session.cookies
session.save()
return response
class Host(object):
"""A host is a per-host directory on the disk containing sessions files."""
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.:-]+$')
def __init__(self, name, root_dir=DEFAULT_SESSIONS_DIR):
assert self.VALID_NAME_PATTERN.match(name)
self.name = name
self.root_dir = root_dir
def __iter__(self):
"""Return an iterator yielding `Session` instances."""
for fn in sorted(glob.glob1(self.path, '*.json')):
session_name = os.path.splitext(fn)[0]
yield Session(host=self, name=session_name)
@staticmethod
def _quote_name(name):
"""host:port => host_port"""
return name.replace(':', '_')
@staticmethod
def _unquote_name(name):
"""host_port => host:port"""
return re.sub(r'_(\d+)$', r':\1', name)
@classmethod
def all(cls, root_dir=DEFAULT_SESSIONS_DIR):
"""Return a generator yielding a host at a time."""
for name in sorted(glob.glob1(root_dir, '*')):
if os.path.isdir(os.path.join(root_dir, name)):
yield Host(cls._unquote_name(name), root_dir=root_dir)
@property
def verbose_name(self):
return '%s %s' % (self.name, self.path)
def delete(self):
shutil.rmtree(self.path)
@property
def path(self):
path = os.path.join(self.root_dir, self._quote_name(self.name))
try:
os.makedirs(path, mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return path
class Session(BaseConfigDict):
help = 'https://github.com/jkbr/httpie#sessions'
about = 'HTTPie session file'
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
def __init__(self, host, name, *args, **kwargs):
assert self.VALID_NAME_PATTERN.match(name)
super(Session, self).__init__(*args, **kwargs)
self.host = host
self.name = name
self['headers'] = {}
self['cookies'] = {}
self['auth'] = {
'type': None,
'username': None,
'password': None
}
@property
def directory(self):
return self.host.path
@property
def verbose_name(self):
return '%s %s %s' % (self.host.name, self.name, self.path)
@property
def cookies(self):
jar = RequestsCookieJar()
for name, cookie_dict in self['cookies'].items():
jar.set_cookie(create_cookie(
name, cookie_dict.pop('value'), **cookie_dict))
jar.clear_expired_cookies()
return jar
@cookies.setter
def cookies(self, jar):
# http://docs.python.org/2/library/cookielib.html#cookie-objects
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for host in jar._cookies.values():
for path in host.values():
for name, cookie in path.items():
self['cookies'][name] = dict(
(attname, getattr(cookie, attname))
for attname in stored_attrs
)
@property
def auth(self):
auth = self.get('auth', None)
if not auth or not auth['type']:
return
Auth = {'basic': HTTPBasicAuth,
'digest': HTTPDigestAuth}[auth['type']]
return Auth(auth['username'], auth['password'])
@auth.setter
def auth(self, cred):
self['auth'] = {
'type': {HTTPBasicAuth: 'basic',
HTTPDigestAuth: 'digest'}[type(cred)],
'username': cred.username,
'password': cred.password,
}

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
#

View File

@ -1,5 +1,7 @@
import os
import sys
import re
import codecs
from setuptools import setup
import httpie
@ -10,8 +12,7 @@ if sys.argv[-1] == 'test':
requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'requests>=0.10.1',
'requests>=1.0.4',
'Pygments>=1.5'
]
if sys.version_info[:2] in ((2, 6), (3, 1)):
@ -22,11 +23,20 @@ if 'win32' in str(sys.platform).lower():
requirements.append('colorama>=0.2.4')
def long_description():
"""Pre-process the README so that PyPi can render it properly."""
with codecs.open('README.rst', encoding='utf8') as f:
rst = f.read()
code_block = '(:\n\n)?\.\. code-block::.*'
rst = re.sub(code_block, '::', rst)
return rst
setup(
name='httpie',
version=httpie.__version__,
description=httpie.__doc__.strip(),
long_description=open('README.rst').read(),
long_description=long_description(),
url='http://httpie.org/',
download_url='https://github.com/jkbr/httpie',
author=httpie.__author__,

View File

@ -19,19 +19,23 @@ To make it run faster and offline you can::
HTTPBIN_URL=http://localhost:5000 tox
"""
import subprocess
import os
import sys
import json
import argparse
import tempfile
import unittest
import shutil
try:
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen
try:
from unittest import skipIf
from unittest import skipIf, skip
except ImportError:
skip = lambda msg: lambda self: None
def skipIf(cond, reason):
def decorator(test_method):
@ -40,9 +44,7 @@ except ImportError:
return test_method
return decorator
from requests.compat import is_windows, is_py26, bytes, str
from requests import __version__ as requests_version
#################################################################
@ -53,14 +55,16 @@ from requests.compat import is_windows, is_py26, bytes, str
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import EXIT
from httpie import ExitStatus
from httpie import input
from httpie.models import Environment
from httpie.core import main
from httpie.output import BINARY_SUPPRESSED_NOTICE
from httpie.input import ParseError
from httpie.compat import is_windows, is_py26, bytes, str
CRLF = '\r\n'
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
'http://httpbin.org').rstrip('/')
@ -97,11 +101,16 @@ def httpbin(path):
return HTTPBIN_URL + path
def mk_config_dir():
return tempfile.mkdtemp(prefix='httpie_test_config_dir_')
class TestEnvironment(Environment):
colors = 0
stdin_isatty = True,
stdout_isatty = True
is_windows = False
_shutil = shutil # we need it in __del__ (would get gc'd)
def __init__(self, **kwargs):
@ -111,11 +120,43 @@ class TestEnvironment(Environment):
if 'stderr' not in kwargs:
kwargs['stderr'] = tempfile.TemporaryFile('w+t')
self.delete_config_dir = False
if 'config_dir' not in kwargs:
kwargs['config_dir'] = mk_config_dir()
self.delete_config_dir = True
super(TestEnvironment, self).__init__(**kwargs)
def __del__(self):
if self.delete_config_dir:
self._shutil.rmtree(self.config_dir)
def has_docutils():
try:
#noinspection PyUnresolvedReferences
import docutils
return True
except ImportError:
return False
def get_readme_errors():
p = subprocess.Popen([
'rst2pseudoxml.py',
'--report=1',
'--exit-status=1',
os.path.join(TESTS_ROOT, '..', 'README.rst')
], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
err = p.communicate()[1]
if p.returncode:
return err
class BytesResponse(bytes):
stderr = json = exit_status = None
class StrResponse(str):
stderr = json = exit_status = None
@ -142,18 +183,17 @@ def http(*args, **kwargs):
try:
try:
exit_status = main(args=['--debug'] + list(args), **kwargs)
exit_status = main(args=['--traceback'] + list(args), **kwargs)
except Exception:
sys.stderr.write(env.stderr.read())
raise
except SystemExit:
exit_status = EXIT.ERROR
exit_status = ExitStatus.ERROR
env.stdout.seek(0)
env.stderr.seek(0)
output = env.stdout.read()
try:
r = StrResponse(output.decode('utf8'))
except UnicodeDecodeError:
@ -166,7 +206,7 @@ def http(*args, **kwargs):
r.json = json.loads(r)
elif r.count('Content-Type:') == 1 and 'application/json' in r:
try:
j = r.strip()[r.strip().rindex('\n\n'):]
j = r.strip()[r.strip().rindex('\r\n\r\n'):]
except ValueError:
pass
else:
@ -187,6 +227,8 @@ def http(*args, **kwargs):
class BaseTestCase(unittest.TestCase):
maxDiff = 100000
if is_py26:
def assertIn(self, member, container, msg=None):
self.assertTrue(member in container, msg)
@ -502,8 +544,8 @@ class ImplicitHTTPMethodTest(BaseTestCase):
self.assertIn(OK, r)
class PrettyFlagTest(BaseTestCase):
"""Test the --pretty / --ugly flag handling."""
class PrettyOptionsTest(BaseTestCase):
"""Test the --pretty flag handling."""
def test_pretty_enabled_by_default(self):
r = http(
@ -522,7 +564,7 @@ class PrettyFlagTest(BaseTestCase):
def test_force_pretty(self):
r = http(
'--pretty',
'--pretty=all',
'GET',
httpbin('/get'),
env=TestEnvironment(stdout_isatty=False, colors=256),
@ -531,7 +573,7 @@ class PrettyFlagTest(BaseTestCase):
def test_force_ugly(self):
r = http(
'--ugly',
'--pretty=none',
'GET',
httpbin('/get'),
)
@ -544,7 +586,7 @@ class PrettyFlagTest(BaseTestCase):
"""
r = http(
'--print=B',
'--pretty',
'--pretty=all',
httpbin('/post'),
'Content-Type:text/foo+json',
'a=b',
@ -552,6 +594,34 @@ class PrettyFlagTest(BaseTestCase):
)
self.assertIn(COLOR, r)
def test_colors_option(self):
r = http(
'--print=B',
'--pretty=colors',
'GET',
httpbin('/get'),
'a=b',
env=TestEnvironment(colors=256),
)
#noinspection PyUnresolvedReferences
# Tests that the JSON data isn't formatted.
self.assertEqual(r.strip().count('\n'), 0)
self.assertIn(COLOR, r)
def test_format_option(self):
r = http(
'--print=B',
'--pretty=format',
'GET',
httpbin('/get'),
'a=b',
env=TestEnvironment(colors=256),
)
#noinspection PyUnresolvedReferences
# Tests that the JSON data is formatted.
self.assertEqual(r.strip().count('\n'), 2)
self.assertNotIn(COLOR, r)
class VerboseFlagTest(BaseTestCase):
@ -688,7 +758,7 @@ class BinaryResponseDataTest(BaseTestCase):
def test_binary_suppresses_when_not_terminal_but_pretty(self):
r = http(
'--pretty',
'--pretty=all',
'GET',
self.url,
env=TestEnvironment(stdin_isatty=True,
@ -765,6 +835,8 @@ class AuthTest(BaseTestCase):
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
@skipIf(requests_version == '0.13.6',
'Redirects with prefetch=False are broken in Requests 0.13.6')
def test_digest_auth(self):
r = http(
'--auth-type=digest',
@ -791,6 +863,31 @@ class AuthTest(BaseTestCase):
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url(self):
url = httpbin('/basic-auth/user/password')
url = 'http://user:password@' + url.split('http://', 1)[1]
r = http(
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url_auth_flag_has_priority(self):
"""When credentials are passed in URL and via -a at the same time,
then the ones from -a are used."""
url = httpbin('/basic-auth/user/password')
url = 'http://user:wrong_password@' + url.split('http://', 1)[1]
r = http(
'--auth=user:password',
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
class ExitStatusTest(BaseTestCase):
@ -800,7 +897,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/200')
)
self.assertIn(OK, r)
self.assertEqual(r.exit_status, EXIT.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
def test_error_response_exits_0_without_check_status(self):
r = http(
@ -808,7 +905,16 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, EXIT.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
self.assertTrue(not r.stderr)
def test_timeout_exit_status(self):
r = http(
'--timeout=0.5',
'GET',
httpbin('/delay/1')
)
self.assertEqual(r.exit_status, ExitStatus.ERROR_TIMEOUT)
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
r = http(
@ -819,19 +925,21 @@ class ExitStatusTest(BaseTestCase):
env=TestEnvironment(stdout_isatty=False,)
)
self.assertIn('HTTP/1.1 301', r)
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_3XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_3XX)
self.assertIn('301 moved permanently', r.stderr.lower())
@skipIf(requests_version == '0.13.6',
'Redirects with prefetch=False are broken in Requests 0.13.6')
def test_3xx_check_status_redirects_allowed_exits_0(self):
r = http(
'--check-status',
'--allow-redirects',
'--follow',
'GET',
httpbin('/status/301')
)
# The redirect will be followed so 200 is expected.
self.assertIn('HTTP/1.1 200 OK', r)
self.assertEqual(r.exit_status, EXIT.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
def test_4xx_check_status_exits_4(self):
r = http(
@ -840,7 +948,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/401')
)
self.assertIn('HTTP/1.1 401', r)
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_4XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_4XX)
# Also stderr should be empty since stdout isn't redirected.
self.assertTrue(not r.stderr)
@ -851,34 +959,32 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_5XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_5XX)
class WindowsOnlyTests(BaseTestCase):
@skip('FIXME: kills the runner')
#@skipIf(not is_windows, 'windows-only')
def test_windows_colorized_output(self):
# Spits out the colorized output.
http(httpbin('/get'), env=Environment())
class FakeWindowsTest(BaseTestCase):
def test_stdout_redirect_not_supported_on_windows(self):
env = TestEnvironment(is_windows=True, stdout_isatty=False)
r = http(
'GET',
httpbin('/get'),
env=env
)
self.assertNotEqual(r.exit_status, EXIT.OK)
self.assertIn('Windows', r.stderr)
self.assertIn('--output', r.stderr)
def test_output_file_pretty_not_allowed_on_windows(self):
r = http(
'--output',
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
'--pretty',
'--pretty=all',
'GET',
httpbin('/get'),
env=TestEnvironment(is_windows=True)
)
self.assertIn(
'Only terminal output can be prettified on Windows', r.stderr)
'Only terminal output can be colorized on Windows', r.stderr)
class StreamTest(BaseTestCase):
@ -890,7 +996,7 @@ class StreamTest(BaseTestCase):
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--verbose',
'--pretty',
'--pretty=all',
'--stream',
'GET',
httpbin('/get'),
@ -906,10 +1012,11 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK_COLOR, r)
def test_encoded_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--ugly',
'--pretty=none',
'--stream',
'--verbose',
'GET',
@ -924,10 +1031,11 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK, r)
def test_redirected_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--ugly',
'--pretty=none',
'--stream',
'--verbose',
'GET',
@ -943,6 +1051,67 @@ class StreamTest(BaseTestCase):
self.assertIn(BIN_FILE_CONTENT, r)
class LineEndingsTest(BaseTestCase):
"""Test that CRLF is properly used in headers and
as the headers/body separator."""
def _validate_crlf(self, msg):
lines = iter(msg.splitlines(True))
for header in lines:
if header == CRLF:
break
self.assertTrue(header.endswith(CRLF), repr(header))
else:
self.fail('CRLF between headers and body not found in %r' % msg)
body = ''.join(lines)
self.assertNotIn(CRLF, body)
return body
def test_CRLF_headers_only(self):
r = http(
'--headers',
'GET',
httpbin('/get')
)
body = self._validate_crlf(r)
self.assertFalse(body, 'Garbage after headers: %r' % r)
def test_CRLF_ugly_response(self):
r = http(
'--pretty=none',
'GET',
httpbin('/get')
)
self._validate_crlf(r)
def test_CRLF_formatted_response(self):
r = http(
'--pretty=format',
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status, 0)
self._validate_crlf(r)
def test_CRLF_ugly_request(self):
r = http(
'--pretty=none',
'--print=HB',
'GET',
httpbin('/get')
)
self._validate_crlf(r)
def test_CRLF_formatted_request(self):
r = http(
'--pretty=format',
'--print=HB',
'GET',
httpbin('/get')
)
self._validate_crlf(r)
#################################################################
# CLI argument parsing related tests.
#################################################################
@ -1093,11 +1262,153 @@ class ArgumentParserTestCase(unittest.TestCase):
self.assertEqual(args.items, [
input.KeyValue(
key='new_item', value='a', sep='=', orig='new_item=a'),
input.KeyValue(key
='old_item', value='b', sep='=', orig='old_item=b'),
input.KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b'),
])
class TestNoOptions(BaseTestCase):
def test_valid_no_options(self):
r = http(
'--verbose',
'--no-verbose',
'GET',
httpbin('/get')
)
self.assertNotIn('GET /get HTTP/1.1', r)
def test_invalid_no_options(self):
r = http(
'--no-war',
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status, 1)
self.assertIn('unrecognized arguments: --no-war', r.stderr)
self.assertNotIn('GET /get HTTP/1.1', r)
class READMETest(BaseTestCase):
@skipIf(not has_docutils(), 'docutils not installed')
def test_README_reStructuredText_valid(self):
errors = get_readme_errors()
self.assertFalse(errors, msg=errors)
class SessionTest(BaseTestCase):
@property
def env(self):
return TestEnvironment(config_dir=self.config_dir)
def setUp(self):
# Start a full-blown session with a custom request header,
# authorization, and response cookies.
self.config_dir = mk_config_dir()
r = http(
'--follow',
'--session=test',
'--auth=username:password',
'GET',
httpbin('/cookies/set?hello=world'),
'Hello:World',
env=self.env
)
self.assertIn(OK, r)
def tearDown(self):
shutil.rmtree(self.config_dir)
def test_session_create(self):
# Verify that the session has been created.
r = http(
'--session=test',
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r)
self.assertEqual(r.json['headers']['Hello'], 'World')
self.assertEqual(r.json['headers']['Cookie'], 'hello=world')
self.assertIn('Basic ', r.json['headers']['Authorization'])
def test_session_update(self):
# Get a response to a request from the original session.
r1 = http(
'--session=test',
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r1)
# Make a request modifying the session data.
r2 = http(
'--follow',
'--session=test',
'--auth=username:password2',
'GET',
httpbin('/cookies/set?hello=world2'),
'Hello:World2',
env=self.env
)
self.assertIn(OK, r2)
# Get a response to a request from the updated session.
r3 = http(
'--session=test',
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r3)
self.assertEqual(r3.json['headers']['Hello'], 'World2')
self.assertEqual(r3.json['headers']['Cookie'], 'hello=world2')
self.assertNotEqual(r1.json['headers']['Authorization'],
r3.json['headers']['Authorization'])
def test_session_read_only(self):
# Get a response from the original session.
r1 = http(
'--session=test',
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r1)
# Make a request modifying the session data but
# with --session-read-only.
r2 = http(
'--follow',
'--session-read-only=test',
'--auth=username:password2',
'GET',
httpbin('/cookies/set?hello=world2'),
'Hello:World2',
env=self.env
)
self.assertIn(OK, r2)
# Get a response from the updated session.
r3 = http(
'--session=test',
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r3)
# Origin can differ on Travis.
del r1.json['origin'], r3.json['origin']
# Should be the same as before r2.
self.assertDictEqual(r1.json, r3.json)
if __name__ == '__main__':
#noinspection PyCallingNonCallable
unittest.main()

View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, py32, pypy
envlist = py26, py27, py33, pypy
[testenv]
commands = {envpython} setup.py test