Compare commits

...

49 Commits
0.6.0 ... 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
18 changed files with 801 additions and 319 deletions

View File

@ -29,3 +29,4 @@ Patches and ideas
* `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>`_ * `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
@ -226,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 ``&``
@ -246,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 |
@ -266,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. |
@ -285,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.
@ -332,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
@ -352,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",
}
} }
@ -383,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
@ -391,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+...
----------------- -----------------
@ -416,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
@ -425,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
@ -433,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
@ -458,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
@ -501,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
======= =======
@ -523,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:
@ -541,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
@ -709,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.
------------------------- -------------------------
@ -840,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;
} }
@ -1055,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
@ -1175,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.
======= =======
@ -1191,8 +1264,20 @@ 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.7.0-dev`_ * `0.9.0-dev`_
* `0.6.0`_ * `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. * XML data is now formatted.
* ``--session`` and ``--session-read-only`` now also accept paths to * ``--session`` and ``--session-read-only`` now also accept paths to
session files (eg. ``http --session=/tmp/session.json example.org``). session files (eg. ``http --session=/tmp/session.json example.org``).
@ -1283,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
@ -1293,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
@ -1310,6 +1403,8 @@ Changelog
.. _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: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0 .. _0.6.0: https://github.com/jkbr/httpie/compare/0.5.1...0.6.0
.. _0.7.0-dev: https://github.com/jkbr/httpie/compare/0.6.0...master .. _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.6.0' __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 .plugins.builtin import BuiltinAuthPlugin
from .plugins import plugin_manager
from .sessions import DEFAULT_SESSIONS_DIR 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, SessionNameValidator) 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}
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE) 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).
"""
.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,48 +332,56 @@ 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_validator = SessionNameValidator(
'Session name contains invalid characters.') 'Session name contains invalid characters.'
)
sessions.add_argument( sessions.add_argument(
'--session', '--session',
metavar='SESSION_NAME_OR_PATH', metavar='SESSION_NAME_OR_PATH',
type=session_name_validator, type=session_name_validator,
help=_(''' help="""
Create, or reuse and update a session. Create, or reuse and update a session. Within a session, custom headers,
Within a session, custom headers, auth credential, as well as any auth credential, as well as any cookies sent by the server persist between
cookies sent by the server persist between requests. requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR) Session files are stored in:
{session_dir}/<HOST>/<SESSION_NAME>.json.
"""
.format(session_dir=DEFAULT_SESSIONS_DIR)
) )
sessions.add_argument( sessions.add_argument(
'--session-read-only', '--session-read-only',
metavar='SESSION_NAME_OR_PATH', metavar='SESSION_NAME_OR_PATH',
type=session_name_validator, type=session_name_validator,
help=_(''' help="""
Create or read a session without updating it form the Create or read a session without updating it form the request/response
request/response exchange. 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')
@ -299,26 +389,45 @@ 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')
@ -326,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(
@ -360,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
exit with an error if the status indicates one.
This flag instructs HTTPie to also check the HTTP status code and When the server replies with a 4xx (Client Error) or 5xx (Server Error)
exit with an error if the status indicates one. status code, HTTPie exits with 4 or 5 respectively. If the response is a
3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected.
When the server replies with a 4xx (Client Error) or 5xx """
(Server Error) status code, HTTPie exits with 4 or 5 respectively.
If the response is a 3xx (Redirect) and --follow
hasn't been set, then the exit status is 3.
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,6 +27,7 @@ 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,
session_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,
@ -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

@ -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

@ -10,6 +10,7 @@ import sys
import mimetypes import mimetypes
import threading import threading
from time import sleep, 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 = ProgressReporterThread( 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
) )
@ -260,7 +264,7 @@ class Download(object):
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._progress_reporter.stop() self._progress_reporter.stop()
@ -269,11 +273,11 @@ class Download(object):
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.
@ -282,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):

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:
@ -36,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,
]) ])
@ -114,12 +133,23 @@ 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
self.args.url = scheme + self.args.url
# 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._process_auth() self._process_auth()
return self.args return self.args
@ -183,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:
@ -240,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
@ -253,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:
@ -265,9 +298,12 @@ 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 = (
item.sep in SEP_GROUP_DATA_ITEMS (not self.args.ignore_stdin and
for item in self.args.items not self.env.stdin_isatty) or any(
item.sep in SEP_GROUP_DATA_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
@ -417,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 = ['']
@ -551,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
@ -565,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

@ -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

@ -6,10 +6,10 @@ import os
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'
@ -21,7 +21,8 @@ VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
def get_response(session_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.
@ -50,9 +51,12 @@ def get_response(session_name, requests_kwargs, config_dir, read_only=False):
requests_kwargs['headers'] = dict(session.headers, **request_headers) requests_kwargs['headers'] = dict(session.headers, **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
@ -140,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.2.3', '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):
@ -1198,26 +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` # `requests.structures.CaseInsensitiveDict` => `dict`
headers = dict(headers._store.values()) 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):
@ -1230,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()
@ -1245,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()
@ -1258,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()
@ -1273,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()
@ -1293,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()
@ -1528,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(
@ -1612,9 +1726,9 @@ class DownloadTest(BaseTestCase):
headers={'Content-Length': 10} headers={'Content-Length': 10}
)) ))
time.sleep(1.1) time.sleep(1.1)
download._chunk_downloaded(b'12345') download.chunk_downloaded(b'12345')
time.sleep(1.1) time.sleep(1.1)
download._chunk_downloaded(b'12345') download.chunk_downloaded(b'12345')
download.finish() download.finish()
self.assertFalse(download.interrupted) self.assertFalse(download.interrupted)
@ -1622,19 +1736,17 @@ class DownloadTest(BaseTestCase):
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('/')))
time.sleep(1.1) time.sleep(1.1)
download._chunk_downloaded(b'12345') 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)