Compare commits

...

62 Commits
0.5.1 ... 0.8.0

Author SHA1 Message Date
43cc3e7ddb Fixed changelog link. 2014-01-25 15:15:16 +01:00
f1224da526 v0.8.0 2014-01-25 15:11:38 +01:00
e0cc63c7eb Cleanup 2014-01-25 15:09:28 +01:00
52dd6adaa3 Updated README. 2014-01-25 15:04:15 +01:00
1aa77017d5 Catch UnicodeDecodeError when embedding file via =@ or :=@. 2014-01-25 14:57:19 +01:00
748a0a480d Update README.rst 2014-01-17 08:57:05 +01:00
01df344a07 Update README.rst 2014-01-17 08:56:24 +01:00
b1074ccb4f Merge pull request #191 from solidsnack/wip-no-auth-in-host-header
Expunge user:pass@... from Host header.
2014-01-08 02:28:19 -08:00
7a84163d1c Merge pull request #192 from thomasleveil/patch-1
fix typo
2014-01-08 02:27:29 -08:00
a31d552d1c fix typo 2014-01-07 14:04:13 +01:00
5a037b2e13 Expunge user:pass@... from Host header.
In verbose mode, the basic auth user and password would show up in colored
output reporting the Host header, as reported in
https://github.com/jkbr/httpie/issues/169
2014-01-06 19:12:33 +00:00
6af42b1827 Added Bitdeli badge. 2013-12-08 11:38:26 +01:00
0e267d8efa Added a link to the httpie-negotiate auth plugin by @ndzou II. 2013-10-09 23:46:55 +02:00
927acc283e Added a link to the httpie-negotiate auth plugin by @ndzou. 2013-10-09 23:44:55 +02:00
817165f5ff Merge pull request #171 from nlf/master
Allow :port style shorthand for localhost.
2013-10-09 13:22:30 -07:00
4fe3deb9d9 add self to authors, update changelog, and mention shorthand in --help output 2013-10-09 13:21:14 -07:00
9034546b80 tweak readme more 2013-10-09 11:37:05 -07:00
2c12fd99f9 tweak readme more 2013-10-09 11:36:01 -07:00
70eb97dece tweak readme to show http requests 2013-10-09 11:34:22 -07:00
8a52bef559 make shorthand parsing more robust, add unit tests and documentation 2013-10-09 11:32:41 -07:00
711168a899 allow :port style shorthand 2013-10-08 22:41:38 -07:00
81c99886fd Update --proxy examples to include URLs to work with Requests v2.0.0.. 2013-09-25 22:02:29 +02:00
2e535d8345 Fixed password prompt. 2013-09-25 00:17:50 +02:00
0bcd4d2fb0 Fixed a bytes/str issue for Python 3. 2013-09-25 00:00:17 +02:00
d5bc564e4f Allow embeding text (=@) and JSON (:=@) files content into request data fields. 2013-09-24 23:41:18 +02:00
54c5c3d82b 0.7.1 2013-09-24 21:57:29 +02:00
2a6514eb5d Update to requests 2.0.0
Closes #140.
2013-09-24 21:49:43 +02:00
22c2cc6465 Removed unused import. 2013-09-24 20:30:54 +02:00
2265edf05e Cleanup 2013-09-24 20:15:19 +02:00
87774acf5c Changelog 2013-09-24 20:09:23 +02:00
9d2ac5d8ad 0.7.0 2013-09-24 20:07:48 +02:00
3e4e1c72a4 Merge branch 'master' of github.com:jkbr/httpie 2013-09-24 19:51:06 +02:00
29f6b6a2a9 Improved Content-Disposition parsing for --download mode
Closes #168.
2013-09-24 19:50:37 +02:00
26b2d408e7 Merge pull request #167 from matt-hickford/master
Fix plugins ImportError
2013-09-23 02:13:14 -07:00
b5f180a5ee Fix plugins ImportError described at https://github.com/jkbr/httpie/issues/166#issuecomment-24905910 2013-09-23 09:54:06 +01:00
354aaa94bd Improved .netrc example formatting. 2013-09-22 15:20:50 +02:00
2ad4059f92 Improved .netrc example formatting. 2013-09-22 15:19:59 +02:00
5a6b65ecc6 Added link to httpie-oauth. 2013-09-22 15:10:50 +02:00
2acb303552 Added support for auth plugins. 2013-09-21 23:46:15 +02:00
f7b703b4bf Added --ignore-stdin
Closes #150
2013-08-23 10:57:17 +02:00
00de49f4c3 Cleanup 2013-08-18 00:59:10 +02:00
67496162fa Improved --help output. 2013-08-10 11:56:19 +02:00
8378ad3624 Try to import argparse before adding it to reqs. 2013-08-01 09:07:33 +02:00
f87884dd8d README 2013-08-01 08:46:37 +02:00
b671ee35e7 Merge pull request #153 from lorin/patch-1
Augment cookie example in README for multiple cookies
2013-07-31 07:52:22 -07:00
69247066dc Augment cookie example in README for multiple cookies
This change updates the README to show how to pass multiple cookies.
2013-07-31 10:29:38 -04:00
383dba524a Print error when download is interrupted by server
Close #147
2013-07-07 17:00:03 +02:00
60f09776a5 httpless outputs also response headers by default 2013-06-03 12:28:04 +02:00
48719aa70e README 2013-06-03 12:22:34 +02:00
809a461a26 v0.6.0 2013-06-03 12:19:43 +02:00
c3d550e930 Fixed headers tests; Require requests>=1.2.3. 2013-06-02 20:47:29 +02:00
172df162b3 Added XML formatting to CHANGELOG. 2013-06-02 20:27:58 +02:00
1bad62ab0e Handle unicode when formatting XML. 2013-06-02 20:25:36 +02:00
8d302f91f9 Merge branch 'master' of git://github.com/jargonjustin/httpie into jargonjustin-master 2013-06-02 20:14:51 +02:00
63b61bc811 Add custom Host example. 2013-05-20 15:31:02 +02:00
5af88756a6 Fixed download ETA for Python 2.6. 2013-05-14 12:49:29 +02:00
7f624e61b5 Use Thread instead of Timer for progress reporting. 2013-05-14 12:49:03 +02:00
6e848b3203 cleanup 2013-05-14 12:14:08 +02:00
8e112a6948 test_download_no_Content_Length 2013-05-13 15:35:12 +02:00
87c59ae561 Added anonymous sessions (--session=/file/path.json). 2013-05-13 14:47:44 +02:00
76eebeac2a 0.6.0-dev 2013-05-13 12:42:16 +02:00
2e57c080fd Pretty print XML 2012-12-17 13:21:38 -08:00
20 changed files with 990 additions and 462 deletions

View File

@ -28,3 +28,5 @@ Patches and ideas
* `Tomek Wójcik <https://github.com/tomekwojcik>`_ * `Tomek Wójcik <https://github.com/tomekwojcik>`_
* `Davey Shafik <https://github.com/dshafik>`_ * `Davey Shafik <https://github.com/dshafik>`_
* `cido <https://github.com/cido>`_ * `cido <https://github.com/cido>`_
* `Justin Bonnar <https://github.com/jargonjustin>`_
* `Nathan LaFreniere <https://github.com/nlf>`_

View File

@ -121,7 +121,6 @@ See also ``http --help``.
Examples Examples
-------- --------
Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data: Custom `HTTP method`_, `HTTP headers`_ and `JSON`_ data:
.. code-block:: bash .. code-block:: bash
@ -181,6 +180,13 @@ between requests to the same host:
$ http --session=logged-in httpbin.org/headers $ http --session=logged-in httpbin.org/headers
Set a custom ``Host`` header to work around missing DNS records:
.. code-block:: bash
$ http localhost:8000 Host:example.com
.. ..
-------- --------
@ -219,6 +225,42 @@ The only information HTTPie needs to perform a request is a URL.
The default scheme is, somewhat unsurprisingly, ``http://``, The default scheme is, somewhat unsurprisingly, ``http://``,
and can be omitted from the argument ``http example.org`` works just fine. and can be omitted from the argument ``http example.org`` works just fine.
Additionally, curl-like shorthand for localhost is supported.
This means that, for example ``:3000`` would expand to ``http://localhost:3000``
If the port is omitted, then port 80 is assumed.
.. code-block:: bash
$ http :/foo
.. code-block:: http
GET /foo HTTP/1.1
Host: localhost
.. code-block:: bash
$ http :3000/bar
.. code-block:: http
GET /bar HTTP/1.1
Host: localhost:3000
.. code-block:: bash
$ http :
.. code-block:: http
GET / HTTP/1.1
Host: localhost
If find yourself manually constructing URLs with **querystring parameters** If find yourself manually constructing URLs with **querystring parameters**
on the terminal, you may appreciate the ``param==value`` syntax for appending on the terminal, you may appreciate the ``param==value`` syntax for appending
URL parameters so that you don't have to worry about escaping the ``&`` URL parameters so that you don't have to worry about escaping the ``&``
@ -239,14 +281,15 @@ command:
Request Items Request Items
============= =============
There are five different *request item* types that provide a There are a few different *request item* types that provide a
convenient mechanism for specifying HTTP headers, simple JSON and convenient mechanism for specifying HTTP headers, simple JSON and
form data, files, and URL parameters. form data, files, and URL parameters.
They are key/value pairs specified after the URL. All have in They are key/value pairs specified after the URL. All have in
common that they become part of the actual request that is sent and that common that they become part of the actual request that is sent and that
their type is distinguished only by the separator used: their type is distinguished only by the separator used:
``:``, ``=``, ``:=``, ``@``, and ``==``. ``:``, ``=``, ``:=``, ``==``, ``@``, ``=@``, and ``:=@``. The ones with an
``@`` expect a file path as value.
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Item Type | Description | | Item Type | Description |
@ -259,16 +302,16 @@ their type is distinguished only by the separator used:
| | The ``==`` separator is used | | | The ``==`` separator is used |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Data Fields | Request data fields to be serialized as a JSON | | Data Fields | Request data fields to be serialized as a JSON |
| ``field=value`` | object (default), or to be form encoded | | ``field=value``, | object (default), or to be form-encoded |
| | (``--form, -f``). | | ``field=@file.txt`` | (``--form, -f``). |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Raw JSON fields | Useful when sending JSON and one or | | Raw JSON fields | Useful when sending JSON and one or |
| ``field:=json`` | more fields need to be a ``Boolean``, ``Number``, | | ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, |
| | nested ``Object``, or an ``Array``, e.g., | | ``field:=@file.json`` | nested ``Object``, or an ``Array``, e.g., |
| | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` | | | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` |
| | (note the quotes). | | | (note the quotes). |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Files | Only available with ``--form, -f``. | | Form File Fields | Only available with ``--form, -f``. |
| ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. | | ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. |
| | The presence of a file field results | | | The presence of a file field results |
| | in a ``multipart/form-data`` request. | | | in a ``multipart/form-data`` request. |
@ -278,6 +321,8 @@ You can use ``\`` to escape characters that shouldn't be used as separators
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value (or parts thereof). For instance, ``foo\==bar`` will become a data key/value
pair (``foo=`` and ``bar``) instead of a URL parameter. pair (``foo=`` and ``bar``) instead of a URL parameter.
You can also quote values, e.g. ``foo="bar baz"``.
Note that data fields aren't the only way to specify request data: Note that data fields aren't the only way to specify request data:
`Redirected input`_ allows for passing arbitrary data to be sent with the `Redirected input`_ allows for passing arbitrary data to be sent with the
request. request.
@ -325,11 +370,16 @@ Simple example:
Non-string fields use the ``:=`` separator, which allows you to embed raw JSON Non-string fields use the ``:=`` separator, which allows you to embed raw JSON
into the resulting object: into the resulting object. Text and raw JSON files can also be embedded into
fields using ``=@`` and ``:=@``:
.. code-block:: bash .. code-block:: bash
$ http PUT api.example.com/person/1 name=John age:=29 married:=false hobbies:='["http", "pies"]' $ http PUT api.example.com/person/1 \
name=John \
age:=29 married:=false hobbies:='["http", "pies"]' \ # Raw JSON
description=@about-john.txt \ # Embed text file
bookmarks:=@bookmarks.json # Embed JSON file
.. code-block:: http .. code-block:: http
@ -345,8 +395,12 @@ into the resulting object:
"http", "http",
"pies" "pies"
], ],
"description": "John is a nice guy who likes pies.",
"married": false, "married": false,
"name": "John" "name": "John",
"bookmarks": {
"HTTPie": "http://httpie.org",
}
} }
@ -376,7 +430,7 @@ Regular Forms
.. code-block:: bash .. code-block:: bash
$ http --form POST api.example.org/person/1 name='John Smith' email=john@example.org $ http --form POST api.example.org/person/1 name='John Smith' email=john@example.org cv=@~/Documents/cv.txt
.. code-block:: http .. code-block:: http
@ -384,7 +438,7 @@ Regular Forms
POST /person/1 HTTP/1.1 POST /person/1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=utf-8 Content-Type: application/x-www-form-urlencoded; charset=utf-8
name=John+Smith&email=john%40example.org name=John+Smith&email=john%40example.org&cv=John's+CV+...
----------------- -----------------
@ -409,6 +463,9 @@ submitted:
<input type="file" name="cv" /> <input type="file" name="cv" />
</form> </form>
Note that ``@`` is used to simulate a file upload form field, whereas
``=@`` just embeds the file content as a regular text field value.
============ ============
HTTP Headers HTTP Headers
@ -418,7 +475,7 @@ To set custom headers you can use the ``Header:Value`` notation:
.. code-block:: bash .. code-block:: bash
$ http example.org User-Agent:Bacon/1.0 Cookie:valued-visitor=yes X-Foo:Bar Referer:http://httpie.org/ $ http example.org User-Agent:Bacon/1.0 'Cookie:valued-visitor=yes;foo=bar' X-Foo:Bar Referer:http://httpie.org/
.. code-block:: http .. code-block:: http
@ -426,7 +483,7 @@ To set custom headers you can use the ``Header:Value`` notation:
GET / HTTP/1.1 GET / HTTP/1.1
Accept: */* Accept: */*
Accept-Encoding: identity, deflate, compress, gzip Accept-Encoding: identity, deflate, compress, gzip
Cookie: valued-visitor=yes Cookie: valued-visitor=yes;foo=bar
Host: example.org Host: example.org
Referer: http://httpie.org/ Referer: http://httpie.org/
User-Agent: Bacon/1.0 User-Agent: Bacon/1.0
@ -451,8 +508,8 @@ Any of the default headers can be overwritten.
Authentication Authentication
============== ==============
The currently supported authentication schemes are Basic and Digest (more to The currently supported authentication schemes are Basic and Digest
come). There are two flags that control authentication: (see `auth plugins`_ for more). There are two flags that control authentication:
=================== ====================================================== =================== ======================================================
``--auth, -a`` Pass a ``username:password`` pair as ``--auth, -a`` Pass a ``username:password`` pair as
@ -494,19 +551,29 @@ With password prompt:
$ http -a username example.org $ http -a username example.org
Authorization information from your ``.netrc`` file is honored as well: Authorization information from your ``~/.netrc`` file is honored as well:
.. code-block:: bash .. code-block:: bash
$ cat .netrc $ cat ~/.netrc
machine httpbin.org machine httpbin.org
login httpie login httpie
password test password test
$ http httpbin.org/basic-auth/httpie/test $ http httpbin.org/basic-auth/httpie/test
HTTP/1.1 200 OK HTTP/1.1 200 OK
[...] [...]
------------
Auth Plugins
------------
* `httpie-oauth <https://github.com/jkbr/httpie-oauth>`_: OAuth
* `httpie-ntlm <https://github.com/jkbr/httpie-ntlm>`_: NTLM (NT LAN Manager)
* `httpie-negotiate <https://github.com/ndzou/httpie-negotiate>`_: SPNEGO (GSS Negotiate)
======= =======
Proxies Proxies
======= =======
@ -516,7 +583,7 @@ protocol (which is included in the value in case of redirects across protocols):
.. code-block:: bash .. code-block:: bash
$ http --proxy=http:10.10.1.10:3128 --proxy=https:10.10.1.10:1080 example.org $ http --proxy=http:http://10.10.1.10:3128 --proxy=https:https://10.10.1.10:1080 example.org
With Basic authentication: With Basic authentication:
@ -534,8 +601,8 @@ In your ``~/.bash_profile``:
.. code-block:: bash .. code-block:: bash
export HTTP_PROXY=10.10.1.10:3128 export HTTP_PROXY=http://10.10.1.10:3128
export HTTPS_PROXY=10.10.1.10:1080 export HTTPS_PROXY=https://10.10.1.10:1080
export NO_PROXY=localhost,example.com export NO_PROXY=localhost,example.com
@ -702,7 +769,16 @@ On OS X, you can send the contents of the clipboard with ``pbpaste``:
Passing data through ``stdin`` cannot be combined with data fields specified Passing data through ``stdin`` cannot be combined with data fields specified
on the command line. on the command line:
.. code-block:: bash
$ echo 'data' | http POST example.org more=data # This is invalid
To prevent HTTPie from reading ``stdin`` data you can use the
``--ignore-stdin`` option.
------------------------- -------------------------
@ -744,6 +820,7 @@ Also, the following formatting is applied:
* HTTP headers are sorted by name. * HTTP headers are sorted by name.
* JSON data is indented, sorted by keys, and unicode escapes are converted * JSON data is indented, sorted by keys, and unicode escapes are converted
to the characters they represent. to the characters they represent.
* XML data is indented for better readability.
One of these options can be used to control output processing: One of these options can be used to control output processing:
@ -832,7 +909,7 @@ by adding the following to your ``~/.bash_profile``:
function httpless { function httpless {
# `httpless example.org' # `httpless example.org'
http --pretty=all "$@" | less -R; http --pretty=all --print=hb "$@" | less -R;
} }
@ -937,13 +1014,17 @@ Streamed output by small chunks alá ``tail -f``:
Sessions Sessions
======== ========
By default, every request is completely independent of the previous ones. By default, every request is completely independent of any previous ones.
HTTPie also supports persistent sessions, where custom headers (except for the HTTPie also supports persistent sessions, where custom headers (except for the
ones starting with ``Content-`` or ``If-``), authorization, and cookies ones starting with ``Content-`` or ``If-``), authorization, and cookies
(manually specified or sent by the server) persist between requests (manually specified or sent by the server) persist between requests
to the same host. to the same host.
Create a new session named ``user1``: --------------
Named Sessions
--------------
Create a new session named ``user1`` for ``example.org``:
.. code-block:: bash .. code-block:: bash
@ -966,14 +1047,30 @@ To use a session without updating it from the request/response exchange
once it is created, specify the session name via once it is created, specify the session name via
``--session-read-only=SESSION_NAME`` instead. ``--session-read-only=SESSION_NAME`` instead.
Session data are stored in JSON files in the directory Named sessions' data is stored in JSON files in the directory
``~/.httpie/sessions/<host>/<name>.json`` ``~/.httpie/sessions/<host>/<name>.json``
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows). (``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
------------------
Anonymous Sessions
------------------
Instead of a name, you can also directly specify a path to a session file. This
allows for sessions to be re-used across multiple hosts:
.. code-block:: bash
$ http --session=/tmp/session.json example.org
$ http --session=/tmp/session.json admin.example.org
$ http --session=~/.httpie/sessions/another.example.org/test.json example.org
$ http --session-read-only=/tmp/session.json example.org
**Warning:** All session data, including credentials, cookie data, **Warning:** All session data, including credentials, cookie data,
and custom headers are stored in plain text. and custom headers are stored in plain text.
Session files can also be created and edited manually in a text editor. Note that session files can also be created and edited manually in a text
editor; they are plain JSON.
See also `Config`_. See also `Config`_.
@ -1027,14 +1124,18 @@ When using HTTPie from **shell scripts**, it can be handy to set the
``--check-status`` flag. It instructs HTTPie to exit with an error if the ``--check-status`` flag. It instructs HTTPie to exit with an error if the
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
be ``3`` (unless ``--follow`` is set), ``4``, or ``5``, be ``3`` (unless ``--follow`` is set), ``4``, or ``5``,
respectively. Also, the ``--timeout`` option allows to overwrite the default respectively.
30s timeout:
The ``--ignore-stdin`` option prevents HTTPie from reading data from ``stdin``,
which is usually not desirable during non-interactive invocations.
Also, the ``--timeout`` option allows to overwrite the default 30s timeout:
.. code-block:: bash .. code-block:: bash
#!/bin/bash #!/bin/bash
if http --timeout=2.5 --check-status HEAD example.org/health &> /dev/null; then if http --check-status --ignore-stdin --timeout=2.5 HEAD example.org/health &> /dev/null; then
echo 'OK!' echo 'OK!'
else else
case $? in case $? in
@ -1147,7 +1248,7 @@ See `claudiatd/httpie-artwork`_
Authors Authors
======= =======
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_ `Jakub Roztocil`_ (`@jkbrzt`_) created HTTPie and `these fine people`_
have contributed. have contributed.
======= =======
@ -1163,7 +1264,23 @@ Changelog
*You can click a version name to see a diff with the previous one.* *You can click a version name to see a diff with the previous one.*
* `0.6.0-dev`_ * `0.9.0-dev`_
* `0.8.0`_ (2014-01-25)
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
the contents of text and JSON files into request data.
* Added curl-style shorthand for localhost.
* Fixed request ``Host`` header value output so that it doesn't contain
credentials, if included in the URL.
* `0.7.1`_ (2013-09-24)
* Added ``--ignore-stdin``.
* Added support for auth plugins.
* Improved ``--help`` output.
* Improved ``Content-Disposition`` parsing for ``--download`` mode.
* Update to Requests 2.0.0
* `0.6.0`_ (2013-06-03)
* XML data is now formatted.
* ``--session`` and ``--session-read-only`` now also accept paths to
session files (eg. ``http --session=/tmp/session.json example.org``).
* `0.5.1`_ (2013-05-13) * `0.5.1`_ (2013-05-13)
* ``Content-*`` and ``If-*`` request headers are not stored in sessions * ``Content-*`` and ``If-*`` request headers are not stored in sessions
anymore as they are request-specific. anymore as they are request-specific.
@ -1251,6 +1368,14 @@ Changelog
* `0.1.6`_ (2012-03-04) * `0.1.6`_ (2012-03-04)
------------
.. image:: https://d2weczhvl823v0.cloudfront.net/jkbr/httpie/trend.png
:target: https://bitdeli.com/free
:alt: Bitdeli Badge
.. _Requests: http://python-requests.org .. _Requests: http://python-requests.org
.. _Pygments: http://pygments.org/ .. _Pygments: http://pygments.org/
.. _pip: http://www.pip-installer.org/en/latest/index.html .. _pip: http://www.pip-installer.org/en/latest/index.html
@ -1261,8 +1386,8 @@ Changelog
.. _Debian: http://packages.debian.org/httpie .. _Debian: http://packages.debian.org/httpie
.. _the repository: https://github.com/jkbr/httpie .. _the repository: https://github.com/jkbr/httpie
.. _these fine people: https://github.com/jkbr/httpie/contributors .. _these fine people: https://github.com/jkbr/httpie/contributors
.. _Jakub Roztocil: http://roztocil.name .. _Jakub Roztocil: http://subtleapps.com
.. _@jakubroztocil: https://twitter.com/jakubroztocil .. _@jkbrzt: https://twitter.com/jkbrzt
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open .. _existing issues: https://github.com/jkbr/httpie/issues?state=open
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork .. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6 .. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
@ -1277,6 +1402,9 @@ Changelog
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1 .. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
.. _0.5.0: https://github.com/jkbr/httpie/compare/0.4.1...0.5.0 .. _0.5.0: https://github.com/jkbr/httpie/compare/0.4.1...0.5.0
.. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1 .. _0.5.1: https://github.com/jkbr/httpie/compare/0.5.0...0.5.1
.. _0.6.0-dev: https://github.com/jkbr/httpie/compare/0.5.1...master .. _0.6.0: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0
.. _0.7.1: https://github.com/jkbr/httpie/compare/0.6.0...0.7.1
.. _0.8.0: https://github.com/jkbr/httpie/compare/0.7.1...0.8.0
.. _0.9.0-dev: https://github.com/jkbr/httpie/compare/0.8.0...master
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst .. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE .. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE

View File

@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
""" """
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.5.1' __version__ = '0.8.0'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -3,86 +3,147 @@
NOTE: the CLI interface may change before reaching v1.0. NOTE: the CLI interface may change before reaching v1.0.
""" """
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS from textwrap import dedent, wrap
#noinspection PyCompatibility
from argparse import (RawDescriptionHelpFormatter, FileType,
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
from . import __doc__ from . import __doc__
from . import __version__ from . import __version__
from .sessions import DEFAULT_SESSIONS_DIR, Session from .plugins.builtin import BuiltinAuthPlugin
from .plugins import plugin_manager
from .sessions import DEFAULT_SESSIONS_DIR
from .output import AVAILABLE_STYLES, DEFAULT_STYLE from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS, SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD, OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS, OUT_RESP_BODY, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator) PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
def _(text): class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
"""Normalize whitespace.""" """A nicer help formatter.
return ' '.join(text.strip().split())
Help for arguments can be indented and contain new lines.
It will be de-dented and arguments in the help
will be separated by a blank line for better readability.
"""
def __init__(self, max_help_position=6, *args, **kwargs):
# A smaller indent for args help.
kwargs['max_help_position'] = max_help_position
super(HTTPieHelpFormatter, self).__init__(*args, **kwargs)
def _split_lines(self, text, width):
text = dedent(text).strip() + '\n\n'
return text.splitlines()
parser = Parser( parser = Parser(
formatter_class=HTTPieHelpFormatter,
description='%s <http://httpie.org>' % __doc__.strip(), description='%s <http://httpie.org>' % __doc__.strip(),
epilog='For every --option there is a --no-option' epilog=dedent("""
' that reverts the option to its default value.\n\n' For every --OPTION there is also a --no-OPTION that reverts OPTION
'Suggestions and bug reports are greatly appreciated:\n' to its default value.
'https://github.com/jkbr/httpie/issues'
Suggestions and bug reports are greatly appreciated:
https://github.com/jkbr/httpie/issues
""")
) )
############################################################################### #######################################################################
# Positional arguments. # Positional arguments.
############################################################################### #######################################################################
positional = parser.add_argument_group( positional = parser.add_argument_group(
title='Positional arguments', title='Positional arguments',
description=_(''' description=dedent("""
These arguments come after any flags and in the These arguments come after any flags and in the order they are listed here.
order they are listed here. Only URL is required. Only URL is required.
''')
""")
) )
positional.add_argument( positional.add_argument(
'method', 'method',
metavar='METHOD', metavar='METHOD',
nargs=OPTIONAL, nargs=OPTIONAL,
default=None, default=None,
help=_(''' help="""
The HTTP method to be used for the request The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie This argument can be omitted in which case HTTPie will use POST if there
will guess the HTTP method. If there is some is some data to be sent, otherwise GET:
data to be sent, then it will be POST, otherwise GET.
''') $ http example.org # => GET
$ http example.org hello=world # => POST
"""
) )
positional.add_argument( positional.add_argument(
'url', 'url',
metavar='URL', metavar='URL',
help=_(''' help="""
The protocol defaults to http:// if the The scheme defaults to 'http://' if the URL does not include one.
URL does not include one.
''') You can also use a shorthand for localhost
$ http :3000 # => http://localhost:3000
$ http :/foo # => http://localhost/foo
"""
) )
positional.add_argument( positional.add_argument(
'items', 'items',
metavar='REQUEST ITEM', metavar='REQUEST_ITEM',
nargs=ZERO_OR_MORE, nargs=ZERO_OR_MORE,
type=KeyValueArgType(*SEP_GROUP_ITEMS), type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
help=_(''' help=r"""
A key-value pair whose type is defined by the Optional key-value pairs to be included in the request. The separator used
separator used. It can be an HTTP header (header:value), determines the type:
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value), ':' HTTP headers:
a query parameter (name==value),
or a file field (field_name@/path/to/file). Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
You can use a backslash to escape a colliding
separator in the field name. '==' URL parameters to be appended to the request URI:
''')
search==httpie
'=' Data fields to be serialized into a JSON object (with --json, -j)
or form data (with --form, -f):
name=HTTPie language=Python description='CLI HTTP client'
':=' Non-string JSON data fields (only with --json, -j):
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
'@' Form file fields (only with --form, -f):
cs@~/Documents/CV.pdf
'=@' A data field like '=', but takes a file path and embeds its content:
essay=@Documents/essay.txt
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
package:=@./package.json
You can use a backslash to escape a colliding separator in the field name:
field-name-with\:colon=value
"""
) )
############################################################################### #######################################################################
# Content type. # Content type.
############################################################################### #######################################################################
content_type = parser.add_argument_group( content_type = parser.add_argument_group(
title='Predefined content types', title='Predefined content types',
@ -92,29 +153,30 @@ content_type = parser.add_argument_group(
content_type.add_argument( content_type.add_argument(
'--json', '-j', '--json', '-j',
action='store_true', action='store_true',
help=_(''' help="""
(default) Data items from the command (default) Data items from the command line are serialized as a JSON object.
line are serialized as a JSON object. The Content-Type and Accept headers are set to application/json
The Content-Type and Accept headers (if not specified).
are set to application/json (if not specified).
''') """
) )
content_type.add_argument( content_type.add_argument(
'--form', '-f', '--form', '-f',
action='store_true', action='store_true',
help=_(''' help="""
Data items from the command line are serialized as form fields. Data items from the command line are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded
(if not specified). The Content-Type is set to application/x-www-form-urlencoded (if not
The presence of any file fields results specified). The presence of any file fields results in a
in a multipart/form-data request. multipart/form-data request.
''')
"""
) )
############################################################################### #######################################################################
# Output processing # Output processing
############################################################################### #######################################################################
output_processing = parser.add_argument_group(title='Output processing') output_processing = parser.add_argument_group(title='Output processing')
@ -123,12 +185,12 @@ output_processing.add_argument(
dest='prettify', dest='prettify',
default=PRETTY_STDOUT_TTY_ONLY, default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()), choices=sorted(PRETTY_MAP.keys()),
help=_(''' help="""
Controls output processing. The value can be "none" to not prettify Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors the output (default for redirected output), "all" to apply both colors
and formatting and formatting (default for terminal output), "colors", or "format".
(default for terminal output), "colors", or "format".
''') """
) )
output_processing.add_argument( output_processing.add_argument(
'--style', '-s', '--style', '-s',
@ -136,75 +198,97 @@ output_processing.add_argument(
metavar='STYLE', metavar='STYLE',
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
choices=AVAILABLE_STYLES, choices=AVAILABLE_STYLES,
help=_(''' help="""
Output coloring style. One of %s. Defaults to "%s". Output coloring style (default is "{default}"). One of:
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar {available}
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). (e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
"""
.format(
default=DEFAULT_STYLE,
available='\n'.join(
'{0: >20}'.format(line.strip())
for line in
wrap(' '.join(sorted(AVAILABLE_STYLES)), 60)
),
)
) )
############################################################################### #######################################################################
# Output options # Output options
############################################################################### #######################################################################
output_options = parser.add_argument_group(title='Output options') output_options = parser.add_argument_group(title='Output options')
output_options.add_argument( output_options.add_argument(
'--print', '-p', '--print', '-p',
dest='output_options', dest='output_options',
metavar='WHAT', metavar='WHAT',
help=_(''' help="""
String specifying what the output should contain: String specifying what the output should contain:
"{request_headers}" stands for the request headers, and
"{request_body}" for the request body. '{req_head}' request headers
"{response_headers}" stands for the response headers and '{req_body}' request body
"{response_body}" for response the body. '{res_head}' response headers
The default behaviour is "hb" (i.e., the response '{res_body}' response body
headers and body is printed), if standard output is not redirected.
If the output is piped to another program or to a file, The default behaviour is '{default}' (i.e., the response headers and body
then only the body is printed by default. is printed), if standard output is not redirected. If the output is piped
'''.format(request_headers=OUT_REQ_HEAD, to another program or to a file, then only the response body is printed
request_body=OUT_REQ_BODY, by default.
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,)) """
.format(
req_head=OUT_REQ_HEAD,
req_body=OUT_REQ_BODY,
res_head=OUT_RESP_HEAD,
res_body=OUT_RESP_BODY,
default=OUTPUT_OPTIONS_DEFAULT,
)
) )
output_options.add_argument( output_options.add_argument(
'--verbose', '-v', '--verbose', '-v',
dest='output_options', dest='output_options',
action='store_const', action='store_const',
const=''.join(OUTPUT_OPTIONS), const=''.join(OUTPUT_OPTIONS),
help=_(''' help="""
Print the whole request as well as the response. Print the whole request as well as the response. Shortcut for --print={0}.
Shortcut for --print={0}.
'''.format(''.join(OUTPUT_OPTIONS))) """
.format(''.join(OUTPUT_OPTIONS))
) )
output_options.add_argument( output_options.add_argument(
'--headers', '-h', '--headers', '-h',
dest='output_options', dest='output_options',
action='store_const', action='store_const',
const=OUT_RESP_HEAD, const=OUT_RESP_HEAD,
help=_(''' help="""
Print only the response headers. Print only the response headers. Shortcut for --print={0}.
Shortcut for --print={0}.
'''.format(OUT_RESP_HEAD)) """
.format(OUT_RESP_HEAD)
) )
output_options.add_argument( output_options.add_argument(
'--body', '-b', '--body', '-b',
dest='output_options', dest='output_options',
action='store_const', action='store_const',
const=OUT_RESP_BODY, const=OUT_RESP_BODY,
help=_(''' help="""
Print only the response body. Print only the response body. Shortcut for --print={0}.
Shortcut for --print={0}.
'''.format(OUT_RESP_BODY)) """
.format(OUT_RESP_BODY)
) )
output_options.add_argument( output_options.add_argument(
'--stream', '-S', '--stream', '-S',
action='store_true', action='store_true',
default=False, default=False,
help=_(''' help="""
Always stream the output by line, i.e., behave like `tail -f'. Always stream the output by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied), Without --stream and with --pretty (either set or implied),
@ -216,33 +300,31 @@ output_options.add_argument(
It is useful also without --pretty: It ensures that the output is flushed It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks. more often and in smaller chunks.
''') """
) )
output_processing.add_argument( output_options.add_argument(
'--output', '-o', '--output', '-o',
type=FileType('a+b'), type=FileType('a+b'),
dest='output_file', dest='output_file',
metavar='FILE', metavar='FILE',
help=_( help="""
''' Save output to FILE. If --download is set, then only the response body is
Save output to FILE. If --download is set, then only the response saved to the file. Other parts of the HTTP exchange are printed to stderr.
body is saved to the file. Other parts of the HTTP exchange are
printed to stderr. """
'''
)
) )
output_options.add_argument( output_options.add_argument(
'--download', '-d', '--download', '-d',
action='store_true', action='store_true',
default=False, default=False,
help=_(''' help="""
Do not print the response body to stdout. Rather, download it and store it Do not print the response body to stdout. Rather, download it and store it
in a file. The filename is guessed unless specified with --output in a file. The filename is guessed unless specified with --output
[filename]. This action is similar to the default behaviour of wget. [filename]. This action is similar to the default behaviour of wget.
''') """
) )
output_options.add_argument( output_options.add_argument(
@ -250,70 +332,102 @@ output_options.add_argument(
dest='download_resume', dest='download_resume',
action='store_true', action='store_true',
default=False, default=False,
help=_(''' help="""
Resume an interrupted download. Resume an interrupted download. Note that the --output option needs to be
The --output option needs to be specified as well. specified as well.
''')
"""
) )
###############################################################################
#######################################################################
# Sessions # Sessions
############################################################################### #######################################################################
sessions = parser.add_argument_group(title='Sessions')\ sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False) .add_mutually_exclusive_group(required=False)
session_name_validator = SessionNameValidator(
'Session name contains invalid characters.'
)
sessions.add_argument( sessions.add_argument(
'--session', '--session',
metavar='SESSION_NAME', metavar='SESSION_NAME_OR_PATH',
type=RegexValidator( type=session_name_validator,
Session.VALID_NAME_PATTERN, help="""
'Session name contains invalid characters.' 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
help=_(''' requests.
Create, or reuse and update a session.
Within a session, custom headers, auth credential, as well as any Session files are stored in:
cookies sent by the server persist between requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json. {session_dir}/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR)
"""
.format(session_dir=DEFAULT_SESSIONS_DIR)
) )
sessions.add_argument( sessions.add_argument(
'--session-read-only', '--session-read-only',
metavar='SESSION_NAME', metavar='SESSION_NAME_OR_PATH',
help=_(''' type=session_name_validator,
Create or read a session without updating it form the help="""
request/response exchange. Create or read a session without updating it form the request/response
''') exchange.
"""
) )
############################################################################### #######################################################################
# Authentication # Authentication
############################################################################### #######################################################################
# ``requests.request`` keyword arguments. # ``requests.request`` keyword arguments.
auth = parser.add_argument_group(title='Authentication') auth = parser.add_argument_group(title='Authentication')
auth.add_argument( auth.add_argument(
'--auth', '-a', '--auth', '-a',
metavar='USER[:PASS]', metavar='USER[:PASS]',
type=AuthCredentialsArgType(SEP_CREDENTIALS), type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_(''' help="""
If only the username is provided (-a username), If only the username is provided (-a username), HTTPie will prompt
HTTPie will prompt for the password. for the password.
'''),
""",
) )
_auth_plugins = plugin_manager.get_auth_plugins()
auth.add_argument( auth.add_argument(
'--auth-type', '--auth-type',
choices=['basic', 'digest'], choices=[plugin.auth_type for plugin in _auth_plugins],
default='basic', default=_auth_plugins[0].auth_type,
help=_(''' help="""
The authentication mechanism to be used. The authentication mechanism to be used. Defaults to "{default}".
Defaults to "basic".
''') {types}
"""
.format(default=_auth_plugins[0].auth_type, types='\n '.join(
'"{type}": {name}{package}{description}'.format(
type=plugin.auth_type,
name=plugin.name,
package=(
'' if issubclass(plugin, BuiltinAuthPlugin)
else ' (provided by %s)' % plugin.package_name
),
description=(
'' if not plugin.description else
'\n ' + ('\n '.join(wrap(plugin.description)))
)
)
for plugin in _auth_plugins
)),
) )
#######################################################################
# Network # Network
############################################# #######################################################################
network = parser.add_argument_group(title='Network') network = parser.add_argument_group(title='Network')
@ -321,33 +435,34 @@ network.add_argument(
'--proxy', '--proxy',
default=[], default=[],
action='append', action='append',
metavar='PROTOCOL:HOST', metavar='PROTOCOL:PROXY_URL',
type=KeyValueArgType(SEP_PROXY), type=KeyValueArgType(SEP_PROXY),
help=_(''' help="""
String mapping protocol to the URL of the proxy String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128). You can specify multiple (e.g. http:http://foo.bar:3128). You can specify multiple proxies with
proxies with different protocols. different protocols.
''')
"""
) )
network.add_argument( network.add_argument(
'--follow', '--follow',
default=False, default=False,
action='store_true', action='store_true',
help=_(''' help="""
Set this flag if full redirects are allowed Set this flag if full redirects are allowed (e.g. re-POST-ing of data at
(e.g. re-POST-ing of data at new ``Location``) new Location).
''')
"""
) )
network.add_argument( network.add_argument(
'--verify', '--verify',
default='yes', default='yes',
help=_(''' help="""
Set to "no" to skip checking the host\'s SSL certificate. Set to "no" to skip checking the host's SSL certificate. You can also pass
You can also pass the path to a CA_BUNDLE the path to a CA_BUNDLE file for private certs. You can also set the
file for private certs. You can also set REQUESTS_CA_BUNDLE environment variable. Defaults to "yes".
the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes". """
''')
) )
network.add_argument( network.add_argument(
@ -355,63 +470,79 @@ network.add_argument(
type=float, type=float,
default=30, default=30,
metavar='SECONDS', metavar='SECONDS',
help=_(''' help="""
The connection timeout of the request in seconds. The connection timeout of the request in seconds. The default value is
The default value is 30 seconds. 30 seconds.
''')
"""
) )
network.add_argument( network.add_argument(
'--check-status', '--check-status',
default=False, default=False,
action='store_true', action='store_true',
help=_(''' help="""
By default, HTTPie exits with 0 when no network or other fatal By default, HTTPie exits with 0 when no network or other fatal errors
errors occur. occur. This flag instructs HTTPie to also check the HTTP status code and
This flag instructs HTTPie to also check the HTTP status code and
exit with an error if the status indicates one. exit with an error if the status indicates one.
When the server replies with a 4xx (Client Error) or 5xx When the server replies with a 4xx (Client Error) or 5xx (Server Error)
(Server Error) status code, HTTPie exits with 4 or 5 respectively. status code, HTTPie exits with 4 or 5 respectively. If the response is a
If the response is a 3xx (Redirect) and --follow 3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected. Also an error message is written to stderr if stdout is redirected.
''') """
) )
############################################################################### #######################################################################
# Troubleshooting # Troubleshooting
############################################################################### #######################################################################
troubleshooting = parser.add_argument_group(title='Troubleshooting') troubleshooting = parser.add_argument_group(title='Troubleshooting')
troubleshooting.add_argument(
'--ignore-stdin',
action='store_true',
default=False,
help="""
Do not attempt to read stdin.
"""
)
troubleshooting.add_argument( troubleshooting.add_argument(
'--help', '--help',
action='help', action='help',
default=SUPPRESS, default=SUPPRESS,
help='Show this help message and exit' help="""
Show this help message and exit.
"""
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--version', '--version',
action='version', action='version',
version=__version__ version=__version__,
help="""
Show version and exit.
"""
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--traceback', '--traceback',
action='store_true', action='store_true',
default=False, default=False,
help='Prints exception traceback should one occur.' help="""
Prints exception traceback should one occur.
"""
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--debug', '--debug',
action='store_true', action='store_true',
default=False, default=False,
help=_(''' help="""
Prints exception traceback should one occur, and also other Prints exception traceback should one occur, and also other information
information that is useful for debugging HTTPie itself and that is useful for debugging HTTPie itself and for reporting bugs.
for bug reports.
''') """
) )

View File

@ -3,10 +3,10 @@ import sys
from pprint import pformat from pprint import pformat
import requests import requests
import requests.auth
from . import sessions from . import sessions
from . import __version__ from . import __version__
from .plugins import plugin_manager
FORM = 'application/x-www-form-urlencoded; charset=utf-8' FORM = 'application/x-www-form-urlencoded; charset=utf-8'
@ -27,8 +27,9 @@ def get_response(args, config_dir):
response = requests.request(**requests_kwargs) response = requests.request(**requests_kwargs)
else: else:
response = sessions.get_response( response = sessions.get_response(
args=args,
config_dir=config_dir, config_dir=config_dir,
name=args.session or args.session_read_only, session_name=args.session or args.session_read_only,
requests_kwargs=requests_kwargs, requests_kwargs=requests_kwargs,
read_only=bool(args.session_read_only), read_only=bool(args.session_read_only),
) )
@ -69,10 +70,8 @@ def get_requests_kwargs(args):
credentials = None credentials = None
if args.auth: if args.auth:
credentials = { auth_plugin = plugin_manager.get_auth_plugin(args.auth_type)()
'basic': requests.auth.HTTPBasicAuth, credentials = auth_plugin.get_auth(args.auth.key, args.auth.value)
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = { kwargs = {
'stream': True, 'stream': True,

View File

@ -12,7 +12,8 @@ from requests.compat import (
) )
try: try:
#noinspection PyUnresolvedReferences,PyCompatibility
from urllib.parse import urlsplit from urllib.parse import urlsplit
except ImportError: except ImportError:
#noinspection PyUnresolvedReferences,PyCompatibility
from urlparse import urlsplit from urlparse import urlsplit

View File

@ -16,9 +16,8 @@ DEFAULT_CONFIG_DIR = os.environ.get(
class BaseConfigDict(dict): class BaseConfigDict(dict):
name = None name = None
help = None helpurl = None
about = None about = None
directory = DEFAULT_CONFIG_DIR directory = DEFAULT_CONFIG_DIR
def __init__(self, directory=None, *args, **kwargs): def __init__(self, directory=None, *args, **kwargs):
@ -29,18 +28,24 @@ class BaseConfigDict(dict):
def __getattr__(self, item): def __getattr__(self, item):
return self[item] return self[item]
@property def _get_path(self):
def path(self): """Return the config file path without side-effects."""
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') return os.path.join(self.directory, self.name + '.json')
@property
def path(self):
"""Return the config file path creating basedir, if needed."""
path = self._get_path()
try:
os.makedirs(os.path.dirname(path), mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return path
@property @property
def is_new(self): def is_new(self):
return not os.path.exists(self.path) return not os.path.exists(self._get_path())
def load(self): def load(self):
try: try:
@ -61,8 +66,8 @@ class BaseConfigDict(dict):
self['__meta__'] = { self['__meta__'] = {
'httpie': __version__ 'httpie': __version__
} }
if self.help: if self.helpurl:
self['__meta__']['help'] = self.help self['__meta__']['help'] = self.helpurl
if self.about: if self.about:
self['__meta__']['about'] = self.about self['__meta__']['about'] = self.about
@ -82,7 +87,7 @@ class BaseConfigDict(dict):
class Config(BaseConfigDict): class Config(BaseConfigDict):
name = 'config' name = 'config'
help = 'https://github.com/jkbr/httpie#config' helpurl = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file' about = 'HTTPie configuration file'
DEFAULTS = { DEFAULTS = {

View File

@ -18,13 +18,13 @@ from httpie import __version__ as httpie_version
from requests import __version__ as requests_version from requests import __version__ as requests_version
from pygments import __version__ as pygments_version from pygments import __version__ as pygments_version
from .cli import parser
from .compat import str, is_py3 from .compat import str, is_py3
from .client import get_response from .client import get_response
from .downloads import Download from .downloads import Download
from .models import Environment from .models import Environment
from .output import build_output_stream, write, write_with_colors_win_py3 from .output import build_output_stream, write, write_with_colors_win_py3
from . import ExitStatus from . import ExitStatus
from .plugins import plugin_manager
def get_exit_status(http_status, follow=False): def get_exit_status(http_status, follow=False):
@ -58,6 +58,9 @@ def main(args=sys.argv[1:], env=Environment()):
Return exit status code. Return exit status code.
""" """
plugin_manager.load_installed_plugins()
from .cli import parser
if env.config.default_options: if env.config.default_options:
args = env.config.default_options + args args = env.config.default_options + args
@ -132,6 +135,10 @@ def main(args=sys.argv[1:], env=Environment()):
download.finish() download.finish()
if download.interrupted: if download.interrupted:
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
error('Incomplete download: size=%d; downloaded=%d' % (
download.status.total_size,
download.status.downloaded
))
except IOError as e: except IOError as e:
if not traceback and e.errno == errno.EPIPE: if not traceback and e.errno == errno.EPIPE:

View File

@ -9,7 +9,8 @@ import re
import sys import sys
import mimetypes import mimetypes
import threading import threading
from time import time from time import sleep, time
from mailbox import Message
from .output import RawStream from .output import RawStream
from .models import HTTPResponse from .models import HTTPResponse
@ -104,11 +105,14 @@ def filename_from_content_disposition(content_disposition):
""" """
# attachment; filename=jkbr-httpie-0.4.1-20-g40bd8f6.tar.gz # attachment; filename=jkbr-httpie-0.4.1-20-g40bd8f6.tar.gz
match = re.search('filename=(\S+)', content_disposition)
if match and match.group(1): msg = Message('Content-Disposition: %s' % content_disposition)
fn = match.group(1).strip('."') filename = msg.get_filename()
if re.match('^[a-zA-Z0-9._-]+$', fn): if filename:
return fn # Basic sanitation.
filename = os.path.basename(filename).lstrip('.').strip()
if filename:
return filename
def filename_from_url(url, content_type): def filename_from_url(url, content_type):
@ -162,9 +166,9 @@ class Download(object):
self._resumed_from = 0 self._resumed_from = 0
self.finished = False self.finished = False
self._status = Status() self.status = Status()
self._progress_reporter = ProgressReporter( self._progress_reporter = ProgressReporterThread(
status=self._status, status=self.status,
output=progress_file output=progress_file
) )
@ -197,7 +201,7 @@ class Download(object):
:return: RawStream, output_file :return: RawStream, output_file
""" """
assert not self._status.time_started assert not self.status.time_started
try: try:
total_size = int(response.headers['Content-Length']) total_size = int(response.headers['Content-Length'])
@ -232,7 +236,7 @@ class Download(object):
) )
self._output_file = open(get_unique_filename(fn), mode='a+b') self._output_file = open(get_unique_filename(fn), mode='a+b')
self._status.started( self.status.started(
resumed_from=self._resumed_from, resumed_from=self._resumed_from,
total_size=total_size total_size=total_size
) )
@ -241,7 +245,7 @@ class Download(object):
msg=HTTPResponse(response), msg=HTTPResponse(response),
with_headers=False, with_headers=False,
with_body=True, with_body=True,
on_body_chunk_downloaded=self._chunk_downloaded, on_body_chunk_downloaded=self.chunk_downloaded,
chunk_size=1024 * 8 chunk_size=1024 * 8
) )
@ -253,28 +257,27 @@ class Download(object):
self._output_file.name self._output_file.name
) )
) )
self._progress_reporter.report() self._progress_reporter.start()
return stream, self._output_file return stream, self._output_file
def finish(self): def finish(self):
assert not self.finished assert not self.finished
self.finished = True self.finished = True
self._status.finished() self.status.finished()
def failed(self): def failed(self):
self.finish()
self._progress_reporter.stop() self._progress_reporter.stop()
@property @property
def interrupted(self): def interrupted(self):
return ( return (
self.finished self.finished
and self._status.total_size and self.status.total_size
and self._status.total_size != self._status.downloaded and self.status.total_size != self.status.downloaded
) )
def _chunk_downloaded(self, chunk): def chunk_downloaded(self, chunk):
""" """
A download progress callback. A download progress callback.
@ -283,7 +286,7 @@ class Download(object):
:type chunk: bytes :type chunk: bytes
""" """
self._status.chunk_downloaded(len(chunk)) self.status.chunk_downloaded(len(chunk))
class Status(object): class Status(object):
@ -317,7 +320,7 @@ class Status(object):
self.time_finished = time() self.time_finished = time()
class ProgressReporter(object): class ProgressReporterThread(threading.Thread):
""" """
Reports download progress based on its status. Reports download progress based on its status.
@ -330,6 +333,7 @@ class ProgressReporter(object):
:type status: Status :type status: Status
:type output: file :type output: file
""" """
super(ProgressReporterThread, self).__init__()
self.status = status self.status = status
self.output = output self.output = output
self._tick = tick self._tick = tick
@ -338,20 +342,20 @@ class ProgressReporter(object):
self._status_line = '' self._status_line = ''
self._prev_bytes = 0 self._prev_bytes = 0
self._prev_time = time() self._prev_time = time()
self._stop = False self._should_stop = threading.Event()
def stop(self): def stop(self):
"""Stop reporting on next tick.""" """Stop reporting on next tick."""
self._stop = True self._should_stop.set()
def report(self): def run(self):
if self._stop: while not self._should_stop.is_set():
return
if self.status.has_finished: if self.status.has_finished:
self.sum_up() self.sum_up()
else: break
self.report_speed() self.report_speed()
threading.Timer(self._tick, self.report).start() sleep(self._tick)
def report_speed(self): def report_speed(self):
@ -382,7 +386,7 @@ class ProgressReporter(object):
s = int((self.status.total_size - downloaded) / speed) s = int((self.status.total_size - downloaded) / speed)
h, s = divmod(s, 60 * 60) h, s = divmod(s, 60 * 60)
m, s = divmod(s, 60) m, s = divmod(s, 60)
eta = '{}:{:0>2}:{:0>2}'.format(h, m, s) eta = '{0}:{1:0>2}:{2:0>2}'.format(h, m, s)
self._status_line = PROGRESS.format( self._status_line = PROGRESS.format(
percentage=percentage, percentage=percentage,

View File

@ -8,6 +8,7 @@ import json
import mimetypes import mimetypes
import getpass import getpass
from io import BytesIO from io import BytesIO
#noinspection PyCompatibility
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
try: try:
@ -20,6 +21,7 @@ except ImportError:
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from .compat import urlsplit, str from .compat import urlsplit, str
from .sessions import VALID_SESSION_NAME_PATTERN
HTTP_POST = 'POST' HTTP_POST = 'POST'
@ -35,22 +37,40 @@ SEP_PROXY = ':'
SEP_DATA = '=' SEP_DATA = '='
SEP_DATA_RAW_JSON = ':=' SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@' SEP_FILES = '@'
SEP_DATA_EMBED_FILE = '=@'
SEP_DATA_EMBED_RAW_JSON_FILE = ':=@'
SEP_QUERY = '==' SEP_QUERY = '=='
# Separators that become request data # Separators that become request data
SEP_GROUP_DATA_ITEMS = frozenset([ SEP_GROUP_DATA_ITEMS = frozenset([
SEP_DATA, SEP_DATA,
SEP_DATA_RAW_JSON, SEP_DATA_RAW_JSON,
SEP_FILES SEP_FILES,
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE
])
# Separators for items whose value is a filename to be embedded
SEP_GROUP_DATA_EMBED_ITEMS = frozenset([
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE,
])
# Separators for raw JSON items
SEP_GROUP_RAW_JSON_ITEMS = frozenset([
SEP_DATA_RAW_JSON,
SEP_DATA_EMBED_RAW_JSON_FILE,
]) ])
# Separators allowed in ITEM arguments # Separators allowed in ITEM arguments
SEP_GROUP_ITEMS = frozenset([ SEP_GROUP_ALL_ITEMS = frozenset([
SEP_HEADERS, SEP_HEADERS,
SEP_QUERY, SEP_QUERY,
SEP_DATA, SEP_DATA,
SEP_DATA_RAW_JSON, SEP_DATA_RAW_JSON,
SEP_FILES SEP_FILES,
SEP_DATA_EMBED_FILE,
SEP_DATA_EMBED_RAW_JSON_FILE,
]) ])
@ -113,11 +133,22 @@ class Parser(ArgumentParser):
self._process_pretty_options() self._process_pretty_options()
self._guess_method() self._guess_method()
self._parse_items() self._parse_items()
if not env.stdin_isatty: if not self.args.ignore_stdin and not env.stdin_isatty:
self._body_from_file(self.env.stdin) self._body_from_file(self.env.stdin)
if not (self.args.url.startswith((HTTP, HTTPS))): if not (self.args.url.startswith((HTTP, HTTPS))):
# Default to 'https://' if invoked as `https args`. # Default to 'https://' if invoked as `https args`.
scheme = HTTPS if self.env.progname == 'https' else HTTP scheme = HTTPS if self.env.progname == 'https' else HTTP
# See if we're using curl style shorthand for localhost (:3000/foo)
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
if shorthand:
port = shorthand.group(1)
rest = shorthand.group(2)
self.args.url = scheme + 'localhost'
if port:
self.args.url += ':' + port
self.args.url += rest
else:
self.args.url = scheme + self.args.url self.args.url = scheme + self.args.url
self._process_auth() self._process_auth()
@ -182,6 +213,9 @@ class Parser(ArgumentParser):
if self.args.auth: if self.args.auth:
if not self.args.auth.has_password(): if not self.args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt. # Stdin already read (if not a tty) so it's save to prompt.
if self.args.ignore_stdin:
self.error('Unable to prompt for passwords because'
' --ignore-stdin is set.')
self.args.auth.prompt_password(url.netloc) self.args.auth.prompt_password(url.netloc)
elif url.username is not None: elif url.username is not None:
@ -239,7 +273,7 @@ class Parser(ArgumentParser):
if self.args.method is None: if self.args.method is None:
# Invoked as `http URL'. # Invoked as `http URL'.
assert not self.args.items assert not self.args.items
if not self.env.stdin_isatty: if not self.args.ignore_stdin and not self.env.stdin_isatty:
self.args.method = HTTP_POST self.args.method = HTTP_POST
else: else:
self.args.method = HTTP_GET self.args.method = HTTP_GET
@ -252,7 +286,7 @@ class Parser(ArgumentParser):
# Parse the URL as an ITEM and store it as the first ITEM arg. # Parse the URL as an ITEM and store it as the first ITEM arg.
self.args.items.insert( self.args.items.insert(
0, 0,
KeyValueArgType(*SEP_GROUP_ITEMS).__call__(self.args.url) KeyValueArgType(*SEP_GROUP_ALL_ITEMS).__call__(self.args.url)
) )
except ArgumentTypeError as e: except ArgumentTypeError as e:
@ -264,10 +298,13 @@ class Parser(ArgumentParser):
# Set the URL correctly # Set the URL correctly
self.args.url = self.args.method self.args.url = self.args.method
# Infer the method # Infer the method
has_data = not self.env.stdin_isatty or any( has_data = (
(not self.args.ignore_stdin and
not self.env.stdin_isatty) or any(
item.sep in SEP_GROUP_DATA_ITEMS item.sep in SEP_GROUP_DATA_ITEMS
for item in self.args.items for item in self.args.items
) )
)
self.args.method = HTTP_POST if has_data else HTTP_GET self.args.method = HTTP_POST if has_data else HTTP_GET
def _parse_items(self): def _parse_items(self):
@ -373,24 +410,15 @@ class KeyValue(object):
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
def session_name_arg_type(name): class SessionNameValidator(object):
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 __init__(self, error_message):
class RegexValidator(object):
def __init__(self, pattern, error_message):
self.pattern = re.compile(pattern)
self.error_message = error_message self.error_message = error_message
def __call__(self, value): def __call__(self, value):
if not self.pattern.search(value): # Session name can be a path or just a name.
if (os.path.sep not in value
and not VALID_SESSION_NAME_PATTERN.search(value)):
raise ArgumentError(None, self.error_message) raise ArgumentError(None, self.error_message)
return value return value
@ -425,8 +453,8 @@ class KeyValueArgType(object):
"""Tokenize `s`. There are only two token types - strings """Tokenize `s`. There are only two token types - strings
and escaped characters: and escaped characters:
>>> tokenize(r'foo\=bar\\baz') tokenize(r'foo\=bar\\baz')
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz'] => ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
""" """
tokens = [''] tokens = ['']
@ -559,9 +587,7 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
params = ParamDict() params = ParamDict()
for item in items: for item in items:
value = item.value value = item.value
key = item.key
if item.sep == SEP_HEADERS: if item.sep == SEP_HEADERS:
target = headers target = headers
@ -573,21 +599,34 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
value = (os.path.basename(value), value = (os.path.basename(value),
BytesIO(f.read())) BytesIO(f.read()))
except IOError as e: except IOError as e:
raise ParseError( raise ParseError('"%s": %s' % (item.orig, e))
'Invalid argument "%s": %s' % (item.orig, e))
target = files target = files
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]: elif item.sep in SEP_GROUP_DATA_ITEMS:
if item.sep == SEP_DATA_RAW_JSON:
if item.sep in SEP_GROUP_DATA_EMBED_ITEMS:
try: try:
value = json.loads(item.value) with open(os.path.expanduser(value), 'rb') as f:
except ValueError: value = f.read().decode('utf8')
raise ParseError('"%s" is not valid JSON' % item.orig) except IOError as e:
raise ParseError('"%s": %s' % (item.orig, e))
except UnicodeDecodeError:
raise ParseError(
'"%s": cannot embed the content of "%s",'
' not a UTF8 or ASCII-encoded text file'
% (item.orig, item.value)
)
if item.sep in SEP_GROUP_RAW_JSON_ITEMS:
try:
value = json.loads(value)
except ValueError as e:
raise ParseError('"%s": %s' % (item.orig, e))
target = data target = data
else: else:
raise TypeError(item) raise TypeError(item)
target[key] = value target[item.key] = value
return headers, data, files, params return headers, data, files, params

View File

@ -88,16 +88,7 @@ class HTTPMessage(object):
@property @property
def content_type(self): def content_type(self):
"""Return the message content type.""" """Return the message content type."""
ct = self._orig.headers.get( return self._orig.headers.get('Content-Type', '')
b'Content-Type',
self._orig.headers.get(
'Content-Type',
''
)
)
if isinstance(ct, bytes):
ct = ct.decode()
return ct
class HTTPResponse(HTTPMessage): class HTTPResponse(HTTPMessage):
@ -109,6 +100,7 @@ class HTTPResponse(HTTPMessage):
def iter_lines(self, chunk_size): def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size)) return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
#noinspection PyProtectedMember
@property @property
def headers(self): def headers(self):
original = self._orig.raw._original_response original = self._orig.raw._original_response
@ -163,7 +155,7 @@ class HTTPRequest(HTTPMessage):
headers = dict(self._orig.headers) headers = dict(self._orig.headers)
if 'Host' not in headers: if 'Host' not in headers:
headers['Host'] = url.netloc headers['Host'] = url.netloc.split('@')[-1]
headers = ['%s: %s' % (name, value) headers = ['%s: %s' % (name, value)
for name, value in headers.items()] for name, value in headers.items()]

View File

@ -2,6 +2,7 @@
""" """
import json import json
import xml.dom.minidom
from functools import partial from functools import partial
from itertools import chain from itertools import chain
@ -20,6 +21,9 @@ from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY) OUT_RESP_HEAD, OUT_RESP_BODY)
# The default number of spaces to indent when pretty printing
DEFAULT_INDENT = 4
# Colors on Windows via colorama don't look that # Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there. # great and fruity seems to give the best result there.
AVAILABLE_STYLES = set(STYLE_MAP.keys()) AVAILABLE_STYLES = set(STYLE_MAP.keys())
@ -264,8 +268,9 @@ class PrettyStream(EncodedStream):
def _process_body(self, chunk): def _process_body(self, chunk):
return (self.processor return (self.processor
.process_body( .process_body(
chunk.decode(self.msg.encoding, 'replace'), content=chunk.decode(self.msg.encoding, 'replace'),
self.msg.content_type) content_type=self.msg.content_type,
encoding=self.msg.encoding)
.encode(self.output_encoding, 'replace')) .encode(self.output_encoding, 'replace'))
@ -367,12 +372,13 @@ class BaseProcessor(object):
""" """
return headers return headers
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype, encoding):
"""Return processed `content`. """Return processed `content`.
:param content: The body content as text :param content: The body content as text
:param content_type: Full content type, e.g., 'application/atom+xml'. :param content_type: Full content type, e.g., 'application/atom+xml'.
:param subtype: E.g. 'xml'. :param subtype: E.g. 'xml'.
:param encoding: The original content encoding.
""" """
return content return content
@ -381,7 +387,7 @@ class BaseProcessor(object):
class JSONProcessor(BaseProcessor): class JSONProcessor(BaseProcessor):
"""JSON body processor.""" """JSON body processor."""
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype, encoding):
if subtype == 'json': if subtype == 'json':
try: try:
# Indent the JSON data, sort keys by name, and # Indent the JSON data, sort keys by name, and
@ -389,13 +395,29 @@ class JSONProcessor(BaseProcessor):
content = json.dumps(json.loads(content), content = json.dumps(json.loads(content),
sort_keys=True, sort_keys=True,
ensure_ascii=False, ensure_ascii=False,
indent=4) indent=DEFAULT_INDENT)
except ValueError: except ValueError:
# Invalid JSON but we don't care. # Invalid JSON but we don't care.
pass pass
return content return content
class XMLProcessor(BaseProcessor):
"""XML body processor."""
# TODO: tests
def process_body(self, content, content_type, subtype, encoding):
if subtype == 'xml':
try:
# Pretty print the XML
doc = xml.dom.minidom.parseString(content.encode(encoding))
content = doc.toprettyxml(indent=' ' * DEFAULT_INDENT)
except xml.parsers.expat.ExpatError:
# Ignore invalid XML errors (skips attempting to pretty print)
pass
return content
class PygmentsProcessor(BaseProcessor): class PygmentsProcessor(BaseProcessor):
"""A processor that applies syntax-highlighting using Pygments """A processor that applies syntax-highlighting using Pygments
to the headers, and to the body as well if its content type is recognized. to the headers, and to the body as well if its content type is recognized.
@ -427,7 +449,7 @@ class PygmentsProcessor(BaseProcessor):
return pygments.highlight( return pygments.highlight(
headers, HTTPLexer(), self.formatter).strip() headers, HTTPLexer(), self.formatter).strip()
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype, encoding):
try: try:
lexer = self.lexers_by_type.get(content_type) lexer = self.lexers_by_type.get(content_type)
if not lexer: if not lexer:
@ -460,7 +482,8 @@ class OutputProcessor(object):
installed_processors = { installed_processors = {
'format': [ 'format': [
HeadersProcessor, HeadersProcessor,
JSONProcessor JSONProcessor,
XMLProcessor
], ],
'colors': [ 'colors': [
PygmentsProcessor PygmentsProcessor
@ -486,13 +509,18 @@ class OutputProcessor(object):
headers = processor.process_headers(headers) headers = processor.process_headers(headers)
return headers return headers
def process_body(self, content, content_type): def process_body(self, content, content_type, encoding):
# e.g., 'application/atom+xml' # e.g., 'application/atom+xml'
content_type = content_type.split(';')[0] content_type = content_type.split(';')[0]
# e.g., 'xml' # e.g., 'xml'
subtype = content_type.split('/')[-1].split('+')[-1] subtype = content_type.split('/')[-1].split('+')[-1]
for processor in self.processors: for processor in self.processors:
content = processor.process_body(content, content_type, subtype) content = processor.process_body(
content,
content_type,
subtype,
encoding
)
return content return content

View File

@ -0,0 +1,9 @@
from .base import AuthPlugin
from .manager import PluginManager
from .builtin import BasicAuthPlugin, DigestAuthPlugin
plugin_manager = PluginManager()
plugin_manager.register(BasicAuthPlugin)
plugin_manager.register(DigestAuthPlugin)

28
httpie/plugins/base.py Normal file
View File

@ -0,0 +1,28 @@
class AuthPlugin(object):
"""
Base auth plugin class.
See <https://github.com/jkbr/httpie-ntlm> for an example auth plugin.
"""
# The value that should be passed to --auth-type
# to use this auth plugin. Eg. "my-auth"
auth_type = None
# The name of the plugin, eg. "My auth".
name = None
# Optional short description. Will be be shown in the help
# under --auth-type.
description = None
# This be set automatically once the plugin has been loaded.
package_name = None
def get_auth(self, username, password):
"""
Return a ``requests.auth.AuthBase`` subclass instance.
"""
raise NotImplementedError()

26
httpie/plugins/builtin.py Normal file
View File

@ -0,0 +1,26 @@
import requests.auth
from .base import AuthPlugin
class BuiltinAuthPlugin(AuthPlugin):
package_name = '(builtin)'
class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth'
auth_type = 'basic'
def get_auth(self, username, password):
return requests.auth.HTTPBasicAuth(username, password)
class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth'
auth_type = 'digest'
def get_auth(self, username, password):
return requests.auth.HTTPDigestAuth(username, password)

35
httpie/plugins/manager.py Normal file
View File

@ -0,0 +1,35 @@
from pkg_resources import iter_entry_points
ENTRY_POINT_NAMES = [
'httpie.plugins.auth.v1'
]
class PluginManager(object):
def __init__(self):
self._plugins = []
def __iter__(self):
return iter(self._plugins)
def register(self, plugin):
self._plugins.append(plugin)
def get_auth_plugins(self):
return list(self._plugins)
def get_auth_plugin_mapping(self):
return dict((plugin.auth_type, plugin) for plugin in self)
def get_auth_plugin(self, auth_type):
return self.get_auth_plugin_mapping()[auth_type]
def load_installed_plugins(self):
for entry_point_name in ENTRY_POINT_NAMES:
for entry_point in iter_entry_points(entry_point_name):
plugin = entry_point.load()
plugin.package_name = entry_point.dist.key
self.register(entry_point.load())

View File

@ -3,53 +3,60 @@
""" """
import re import re
import os import os
import glob
import errno
import shutil
import requests import requests
from requests.cookies import RequestsCookieJar, create_cookie from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from .compat import urlsplit from .compat import urlsplit
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
from httpie.plugins import plugin_manager
SESSIONS_DIR_NAME = 'sessions' SESSIONS_DIR_NAME = 'sessions'
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME) DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
# Request headers starting with these prefixes won't be stored in sessions. # Request headers starting with these prefixes won't be stored in sessions.
# They are specific to each request. # They are specific to each request.
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests # http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(name, requests_kwargs, config_dir, read_only=False): def get_response(session_name, requests_kwargs, config_dir, args,
read_only=False):
"""Like `client.get_response`, but applies permanent """Like `client.get_response`, but applies permanent
aspects of the session to the request. aspects of the session to the request.
""" """
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME) if os.path.sep in session_name:
host = Host( path = os.path.expanduser(session_name)
root_dir=sessions_dir, else:
name=requests_kwargs['headers'].get('Host', None) hostname = (
requests_kwargs['headers'].get('Host', None)
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1] or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
) )
session = Session(host, name)
assert re.match('^[a-zA-Z0-9_.:-]+$', hostname)
# host:port => host_port
hostname = hostname.replace(':', '_')
path = os.path.join(config_dir,
SESSIONS_DIR_NAME,
hostname,
session_name + '.json')
session = Session(path)
session.load() session.load()
# Merge request and session headers to get final headers for this request.
request_headers = requests_kwargs.get('headers', {}) request_headers = requests_kwargs.get('headers', {})
merged_headers = session.headers.copy() requests_kwargs['headers'] = dict(session.headers, **request_headers)
merged_headers.update(request_headers)
requests_kwargs['headers'] = merged_headers
# Update session headers with the request headers.
session.update_headers(request_headers) session.update_headers(request_headers)
auth = requests_kwargs.get('auth', None) if args.auth:
if auth: session.auth = {
session.auth = auth 'type': args.auth_type,
'username': args.auth.key,
'password': args.auth.value,
}
elif session.auth: elif session.auth:
requests_kwargs['auth'] = session.auth requests_kwargs['auth'] = session.auth
@ -68,69 +75,13 @@ def get_response(name, requests_kwargs, config_dir, read_only=False):
return response 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): class Session(BaseConfigDict):
helpurl = 'https://github.com/jkbr/httpie#sessions'
help = 'https://github.com/jkbr/httpie#sessions'
about = 'HTTPie session file' about = 'HTTPie session file'
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$') def __init__(self, path, *args, **kwargs):
def __init__(self, host, name, *args, **kwargs):
assert self.VALID_NAME_PATTERN.match(name)
super(Session, self).__init__(*args, **kwargs) super(Session, self).__init__(*args, **kwargs)
self.host = host self._path = path
self.name = name
self['headers'] = {} self['headers'] = {}
self['cookies'] = {} self['cookies'] = {}
self['auth'] = { self['auth'] = {
@ -139,13 +90,8 @@ class Session(BaseConfigDict):
'password': None 'password': None
} }
@property def _get_path(self):
def directory(self): return self._path
return self.host.path
@property
def verbose_name(self):
return '%s %s %s' % (self.host.name, self.name, self.path)
def update_headers(self, request_headers): def update_headers(self, request_headers):
""" """
@ -198,15 +144,10 @@ class Session(BaseConfigDict):
auth = self.get('auth', None) auth = self.get('auth', None)
if not auth or not auth['type']: if not auth or not auth['type']:
return return
Auth = {'basic': HTTPBasicAuth, auth_plugin = plugin_manager.get_auth_plugin(auth['type'])()
'digest': HTTPDigestAuth}[auth['type']] return auth_plugin.get_auth(auth['username'], auth['password'])
return Auth(auth['username'], auth['password'])
@auth.setter @auth.setter
def auth(self, cred): def auth(self, auth):
self['auth'] = { assert set(['type', 'username', 'password']) == set(auth.keys())
'type': {HTTPBasicAuth: 'basic', self['auth'] = auth
HTTPDigestAuth: 'digest'}[type(cred)],
'username': cred.username,
'password': cred.password,
}

View File

@ -1,6 +1,5 @@
import os import os
import sys import sys
import re
import codecs import codecs
from setuptools import setup from setuptools import setup
import httpie import httpie
@ -12,24 +11,23 @@ if sys.argv[-1] == 'test':
requirements = [ requirements = [
'requests>=1.0.4', 'requests>=2.0.0',
'Pygments>=1.5' 'Pygments>=1.5'
] ]
if sys.version_info[:2] in ((2, 6), (3, 1)): try:
# argparse has been added in Python 3.2 / 2.7 #noinspection PyUnresolvedReferences
import argparse
except ImportError:
requirements.append('argparse>=1.2.1') requirements.append('argparse>=1.2.1')
if 'win32' in str(sys.platform).lower(): if 'win32' in str(sys.platform).lower():
# Terminal colors for Windows # Terminal colors for Windows
requirements.append('colorama>=0.2.4') requirements.append('colorama>=0.2.4')
def long_description(): def long_description():
"""Pre-process the README so that PyPi can render it properly."""
with codecs.open('README.rst', encoding='utf8') as f: with codecs.open('README.rst', encoding='utf8') as f:
rst = f.read() return f.read()
code_block = '(:\n\n)?\.\. code-block::.*'
rst = re.sub(code_block, '::', rst)
return rst
setup( setup(
@ -42,7 +40,7 @@ setup(
author=httpie.__author__, author=httpie.__author__,
author_email='jakub@roztocil.name', author_email='jakub@roztocil.name',
license=httpie.__licence__, license=httpie.__licence__,
packages=['httpie'], packages=['httpie', 'httpie.plugins'],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'http = httpie.__main__:main', 'http = httpie.__main__:main',

3
tests/fixtures/test.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"hello": "world"
}

View File

@ -23,6 +23,7 @@ import subprocess
import os import os
import sys import sys
import json import json
#noinspection PyCompatibility
import argparse import argparse
import tempfile import tempfile
import unittest import unittest
@ -31,9 +32,11 @@ import time
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
try: try:
#noinspection PyCompatibility
from urllib.request import urlopen from urllib.request import urlopen
except ImportError: except ImportError:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
#noinspection PyCompatibility
from urllib2 import urlopen from urllib2 import urlopen
try: try:
from unittest import skipIf, skip from unittest import skipIf, skip
@ -61,6 +64,7 @@ sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import ExitStatus from httpie import ExitStatus
from httpie import input from httpie import input
from httpie.cli import parser
from httpie.models import Environment from httpie.models import Environment
from httpie.core import main from httpie.core import main
from httpie.output import BINARY_SUPPRESSED_NOTICE from httpie.output import BINARY_SUPPRESSED_NOTICE
@ -98,19 +102,27 @@ def patharg(path):
FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt') FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt') FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin') BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin')
JSON_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'test.json')
FILE_PATH_ARG = patharg(FILE_PATH) FILE_PATH_ARG = patharg(FILE_PATH)
FILE2_PATH_ARG = patharg(FILE2_PATH) FILE2_PATH_ARG = patharg(FILE2_PATH)
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH) BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
with open(FILE_PATH) as f: with open(FILE_PATH) as f:
# Strip because we don't want new lines in the data so that we can
# easily count occurrences also when embedded in JSON (where the new
# line would be escaped).
FILE_CONTENT = f.read().strip() FILE_CONTENT = f.read().strip()
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read() BIN_FILE_CONTENT = f.read()
with open(JSON_FILE_PATH, 'rb') as f:
JSON_FILE_CONTENT = f.read()
def httpbin(path): def httpbin(path):
return HTTPBIN_URL + path url = HTTPBIN_URL + path
return url
def mk_config_dir(): def mk_config_dir():
@ -805,9 +817,8 @@ class RequestBodyFromFilePathTest(BaseTestCase):
""" """
def test_request_body_from_file_by_path(self): def test_request_body_from_file_by_path(self):
# FIXME: *sometimes* fails on py33, the content-type is form.
# https://github.com/jkbr/httpie/issues/140
r = http( r = http(
'--verbose',
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'@' + FILE_PATH_ARG '@' + FILE_PATH_ARG
@ -817,8 +828,6 @@ class RequestBodyFromFilePathTest(BaseTestCase):
self.assertIn('"Content-Type": "text/plain"', r) self.assertIn('"Content-Type": "text/plain"', r)
def test_request_body_from_file_by_path_with_explicit_content_type(self): def test_request_body_from_file_by_path_with_explicit_content_type(self):
# FIXME: *sometimes* fails on py33, the content-type is form.
# https://github.com/jkbr/httpie/issues/140
r = http( r = http(
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
@ -1078,6 +1087,30 @@ class StreamTest(BaseTestCase):
self.assertIn(BIN_FILE_CONTENT, r) self.assertIn(BIN_FILE_CONTENT, r)
class IgnoreStdinTest(BaseTestCase):
def test_ignore_stdin(self):
with open(FILE_PATH) as f:
r = http(
'--ignore-stdin',
'--verbose',
httpbin('/get'),
env=TestEnvironment(stdin=f, stdin_isatty=False)
)
self.assertIn(OK, r)
self.assertIn('GET /get HTTP', r) # Don't default to POST.
self.assertNotIn(FILE_CONTENT, r) # Don't send stdin data.
def test_ignore_stdin_cannot_prompt_password(self):
r = http(
'--ignore-stdin',
'--auth=username-without-password',
httpbin('/get'),
)
self.assertEqual(r.exit_status, ExitStatus.ERROR)
self.assertIn('because --ignore-stdin', r.stderr)
class LineEndingsTest(BaseTestCase): class LineEndingsTest(BaseTestCase):
"""Test that CRLF is properly used in headers and """Test that CRLF is properly used in headers and
as the headers/body separator.""" as the headers/body separator."""
@ -1147,11 +1180,7 @@ class ItemParsingTest(BaseTestCase):
def setUp(self): def setUp(self):
self.key_value_type = input.KeyValueArgType( self.key_value_type = input.KeyValueArgType(
input.SEP_HEADERS, *input.SEP_GROUP_ALL_ITEMS
input.SEP_QUERY,
input.SEP_DATA,
input.SEP_DATA_RAW_JSON,
input.SEP_FILES,
) )
def test_invalid_items(self): def test_invalid_items(self):
@ -1170,6 +1199,8 @@ class ItemParsingTest(BaseTestCase):
# files # files
self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG) self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG)
]) ])
# `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(headers._store.values())
self.assertDictEqual(headers, { self.assertDictEqual(headers, {
'foo:bar': 'baz', 'foo:bar': 'baz',
'jack@jill': 'hill', 'jack@jill': 'hill',
@ -1196,24 +1227,99 @@ class ItemParsingTest(BaseTestCase):
self.key_value_type('eh:'), self.key_value_type('eh:'),
self.key_value_type('ed='), self.key_value_type('ed='),
self.key_value_type('bool:=true'), self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % FILE_PATH_ARG), self.key_value_type('file@' + FILE_PATH_ARG),
self.key_value_type('query==value'), self.key_value_type('query==value'),
self.key_value_type('string-embed=@' + FILE_PATH_ARG),
self.key_value_type('raw-json-embed:=@' + JSON_FILE_PATH_ARG),
]) ])
# Parsed headers
# `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(headers._store.values())
self.assertDictEqual(headers, { self.assertDictEqual(headers, {
'header': 'value', 'header': 'value',
'eh': '' 'eh': ''
}) })
self.assertDictEqual(data, {
# Parsed data
raw_json_embed = data.pop('raw-json-embed')
self.assertDictEqual(raw_json_embed, json.loads(
JSON_FILE_CONTENT.decode('utf8')))
data['string-embed'] = data['string-embed'].strip()
self.assertDictEqual(dict(data), {
"ed": "", "ed": "",
"string": "value", "string": "value",
"bool": True, "bool": True,
"list": ["a", 1, {}, False], "list": ["a", 1, {}, False],
"obj": {"a": "b"}, "obj": {"a": "b"},
"string-embed": FILE_CONTENT,
}) })
# Parsed query string parameters
self.assertDictEqual(params, { self.assertDictEqual(params, {
'query': 'value', 'query': 'value',
}) })
self.assertIn('test-file', files)
# Parsed file fields
self.assertIn('file', files)
self.assertEqual(files['file'][1].read().strip().decode('utf8'),
FILE_CONTENT)
class CLIParserTestCase(unittest.TestCase):
def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost')
def test_expand_localhost_shorthand_with_slash(self):
args = parser.parse_args(args=[':/'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost/')
def test_expand_localhost_shorthand_with_port(self):
args = parser.parse_args(args=[':3000'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost:3000')
def test_expand_localhost_shorthand_with_path(self):
args = parser.parse_args(args=[':/path'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost/path')
def test_expand_localhost_shorthand_with_port_and_slash(self):
args = parser.parse_args(args=[':3000/'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost:3000/')
def test_expand_localhost_shorthand_with_port_and_path(self):
args = parser.parse_args(args=[':3000/path'], env=TestEnvironment())
self.assertEqual(args.url, 'http://localhost:3000/path')
def test_dont_expand_shorthand_ipv6_as_shorthand(self):
args = parser.parse_args(args=['::1'], env=TestEnvironment())
self.assertEqual(args.url, 'http://::1')
def test_dont_expand_longer_ipv6_as_shorthand(self):
args = parser.parse_args(
args=['::ffff:c000:0280'],
env=TestEnvironment()
)
self.assertEqual(args.url, 'http://::ffff:c000:0280')
def test_dont_expand_full_ipv6_as_shorthand(self):
args = parser.parse_args(
args=['0000:0000:0000:0000:0000:0000:0000:0001'],
env=TestEnvironment()
)
self.assertEqual(
args.url,
'http://0000:0000:0000:0000:0000:0000:0000:0001'
)
class ArgumentParserTestCase(unittest.TestCase): class ArgumentParserTestCase(unittest.TestCase):
@ -1226,6 +1332,7 @@ class ArgumentParserTestCase(unittest.TestCase):
self.parser.args.method = 'GET' self.parser.args.method = 'GET'
self.parser.args.url = 'http://example.com/' self.parser.args.url = 'http://example.com/'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment() self.parser.env = TestEnvironment()
@ -1241,6 +1348,7 @@ class ArgumentParserTestCase(unittest.TestCase):
self.parser.args.method = None self.parser.args.method = None
self.parser.args.url = 'http://example.com/' self.parser.args.url = 'http://example.com/'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -1254,6 +1362,7 @@ class ArgumentParserTestCase(unittest.TestCase):
self.parser.args.method = 'http://example.com/' self.parser.args.method = 'http://example.com/'
self.parser.args.url = 'data=field' self.parser.args.url = 'data=field'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment() self.parser.env = TestEnvironment()
self.parser._guess_method() self.parser._guess_method()
@ -1269,6 +1378,7 @@ class ArgumentParserTestCase(unittest.TestCase):
self.parser.args.method = 'http://example.com/' self.parser.args.method = 'http://example.com/'
self.parser.args.url = 'test:header' self.parser.args.url = 'test:header'
self.parser.args.items = [] self.parser.args.items = []
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment() self.parser.env = TestEnvironment()
@ -1289,6 +1399,7 @@ class ArgumentParserTestCase(unittest.TestCase):
input.KeyValue( input.KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b') key='old_item', value='b', sep='=', orig='old_item=b')
] ]
self.parser.args.ignore_stdin = False
self.parser.env = TestEnvironment() self.parser.env = TestEnvironment()
@ -1464,6 +1575,28 @@ class SessionTest(BaseTestCase):
# Should be the same as before r2. # Should be the same as before r2.
self.assertDictEqual(r1.json, r3.json) self.assertDictEqual(r1.json, r3.json)
def test_session_by_path(self):
session_path = os.path.join(self.config_dir, 'session-by-path.json')
r1 = http(
'--session=' + session_path,
'GET',
httpbin('/get'),
'Foo:Bar',
env=self.env
)
self.assertIn(OK, r1)
r2 = http(
'--session=' + session_path,
'GET',
httpbin('/get'),
env=self.env
)
self.assertIn(OK, r2)
self.assertEqual(r2.json['headers']['Foo'], 'Bar')
class DownloadUtilsTest(BaseTestCase): class DownloadUtilsTest(BaseTestCase):
@ -1502,9 +1635,16 @@ class DownloadUtilsTest(BaseTestCase):
parse('attachment; filename=".hello-WORLD_123.txt"'), parse('attachment; filename=".hello-WORLD_123.txt"'),
'hello-WORLD_123.txt' 'hello-WORLD_123.txt'
) )
self.assertEqual(
parse('attachment; filename="white space.txt"'),
'white space.txt'
)
self.assertEqual(
parse(r'attachment; filename="\"quotes\".txt"'),
'"quotes".txt'
)
self.assertEqual(parse('attachment; filename=/etc/hosts'), 'hosts')
self.assertIsNone(parse('attachment; filename=')) self.assertIsNone(parse('attachment; filename='))
self.assertIsNone(parse('attachment; filename=/etc/hosts'))
self.assertIsNone(parse('attachment; filename=hello@world'))
def test_filename_from_url(self): def test_filename_from_url(self):
self.assertEqual(filename_from_url( self.assertEqual(filename_from_url(
@ -1579,22 +1719,34 @@ class DownloadTest(BaseTestCase):
self.assertIn('Done', r.stderr) self.assertIn('Done', r.stderr)
self.assertEqual(body, r) self.assertEqual(body, r)
def test_download_with_Content_Length(self):
download = Download(output_file=open(os.devnull, 'w'))
download.start(Response(
url=httpbin('/'),
headers={'Content-Length': 10}
))
time.sleep(1.1)
download.chunk_downloaded(b'12345')
time.sleep(1.1)
download.chunk_downloaded(b'12345')
download.finish()
self.assertFalse(download.interrupted)
def test_download_no_Content_Length(self): def test_download_no_Content_Length(self):
download = Download(output_file=open(os.devnull, 'w')) download = Download(output_file=open(os.devnull, 'w'))
download.start(Response(url=httpbin('/'))) download.start(Response(url=httpbin('/')))
download._chunk_downloaded(b'12345') time.sleep(1.1)
download.chunk_downloaded(b'12345')
download.finish() download.finish()
self.assertFalse(download.interrupted) self.assertFalse(download.interrupted)
def test_download_interrupted(self): def test_download_interrupted(self):
download = Download( download = Download(output_file=open(os.devnull, 'w'))
output_file=open(os.devnull, 'w')
)
download.start(Response( download.start(Response(
url=httpbin('/'), url=httpbin('/'),
headers={'Content-Length': 5} headers={'Content-Length': 5}
)) ))
download._chunk_downloaded(b'1234') download.chunk_downloaded(b'1234')
download.finish() download.finish()
self.assertTrue(download.interrupted) self.assertTrue(download.interrupted)