Compare commits

..

45 Commits
0.3.1 ... 0.4.0

Author SHA1 Message Date
d4f2daca56 v0.4.0 2013-02-22 13:52:05 +01:00
d97a610f7c Added new logo by @claudiatd 2013-02-22 13:51:37 +01:00
5cc5b13555 Removed the management command.
It means that:

    httpie session list
    httpie session edit
    ...

are gone.

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
***********************
HTTPie: cURL for Humans
***********************
****************************************
HTTPie: a CLI, cURL-like tool for humans
****************************************
v0.3.0
.. image:: https://raw.github.com/claudiatd/httpie-artwork/master/images/httpie_logo_simple.png
:alt: HTTPie logo
:width: 469
:height: 331
:align: center
HTTPie is a **command line HTTP client** whose goal is to make CLI interaction
v0.4.0-alpha (`stable version`_)
HTTPie is a **command line HTTP client**. Its goal is to make CLI interaction
with web services as **human-friendly** as possible. It provides a
simple ``http`` command that allows for sending arbitrary HTTP requests with a
simple ``http`` command that allows for sending arbitrary HTTP requests using a
simple and natural syntax, and displays colorized responses. HTTPie can be used
for **testing, debugging**, and generally **interacting** with HTTP servers.
@ -15,10 +21,11 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
:alt: HTTPie compared to cURL
:width: 835
:height: 835
:align: center
HTTPie is written in Python, and under the hood it uses the excellent
`Requests`_ for HTTP and `Pygments`_ for colorizing.
`Requests`_ and `Pygments`_ libraries.
**Table of Contents**
@ -59,9 +66,11 @@ or ``easy_install``:
.. code-block:: bash
$ pip install -U httpie
$ pip install --upgrade httpie
Alternatively:
.. code-block:: bash
$ easy_install httpie
@ -77,12 +86,12 @@ Or, you can install the **development version** directly from GitHub:
.. code-block:: bash
$ pip install -U https://github.com/jkbr/httpie/tarball/master
$ pip install --upgrade https://github.com/jkbr/httpie/tarball/master
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
Linux distributions as well. However, they may be a significant delay between
releases and package updates.
Linux distributions as well. However, there may be a significant delay between
official HTTPie releases and package updates.
=====
@ -127,7 +136,7 @@ Submitting `forms`_:
$ http -f POST example.org hello=World
See the request that is being sent using on of the `output options`_:
See the request that is being sent using one of the `output options`_:
.. code-block:: bash
@ -154,11 +163,21 @@ Download a file and save it via `redirected output`_:
$ http example.org/file > file
Use named `sessions`_ to make certain aspects or the communication persistent
between requests to the same host:
.. code-block:: bash
$ http --session=logged-in -a username:password httpbin.org/get API-Key:123
$ http --session=logged-in httpbin.org/headers
..
--------
*What follows is a detailed documentation. It covers the command syntax,
advances usage, and also features additional examples.*
advanced usage, and also features additional examples.*
============
@ -247,11 +266,12 @@ their type is distinguished only by the separator used:
+-----------------------+-----------------------------------------------------+
You can use ``\`` to escape characters that shouldn't be used as separators
(or parts thereof). e.g., ``foo\==bar`` will become a data key/value
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
pair (``foo=`` and ``bar``) instead of a URL parameter.
Note that data fields aren't the only way to specify request data,
`redirected input`_ allows passing arbitrary data to be sent with the request.
Note that data fields aren't the only way to specify request data:
`Redirected input`_ allows for passing arbitrary data to be sent with the
request.
====
@ -339,8 +359,8 @@ difference is in adding the ``--form`` / ``-f`` option, which ensures that
data fields are serialized as, and ``Content-Type`` is set to,
``application/x-www-form-urlencoded; charset=utf-8``.
It is possible to make form data the implicit content type via the `config`_
file.
It is possible to make form data the implicit content type instead of JSON
via the `config`_ file.
-------------
@ -434,13 +454,16 @@ come). There are two flags that control authentication:
(``-a username``), you'll be prompted for
the password before the request is sent.
To send a an empty password, pass ``username:``.
The ``username:password@hostname`` URL syntax is
supported as well (but credentials passed via ``-a``
have higher priority).
``--auth-type`` Specify the auth mechanism. Possible values are
``basic`` and ``digest``. The default value is
``basic`` so it can often be omitted.
=================== ======================================================
Authorization information from ``.netrc`` is respected as well.
Authorization information from ``.netrc`` is honored as well.
Basic auth:
@ -473,14 +496,14 @@ You can specify proxies to be used through the ``--proxy`` argument:
.. code-block:: bash
http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
$ http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
With Basic authentication:
.. code-block:: bash
http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
$ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
You can also configure proxies by environment variables ``HTTP_PROXY`` and
``HTTPS_PROXY``, and the underlying Requests library will pick them up as well.
@ -724,7 +747,7 @@ that the response body is binary,
.. code-block:: bash
http example.org/Movie.mov
$ http example.org/Movie.mov
You will nearly instantly see something like this:
@ -782,6 +805,16 @@ Force colorizing and formatting, and show both the request and the response in
The ``-R`` flag tells ``less`` to interpret color escape sequences included
HTTPie`s output.
You can create a shortcut for invoking HTTPie with colorized and paged output
by adding the following to your ``~/.bash_profile``:
.. code-block:: bash
function httpless {
# `httpless example.org'
http --pretty=all "$@" | less -R;
}
==================
Streamed Responses
@ -827,10 +860,9 @@ Sessions
========
By default, every request is completely independent of the previous ones.
HTTPie supports persistent sessions, where custom headers, authorization,
HTTPie also supports persistent sessions, where custom headers, authorization,
and cookies (manually specified or sent by the server) persist between
requests. Sessions are named and host-bound.
requests to the same host.
Create a new session named ``user1``:
@ -838,7 +870,8 @@ Create a new session named ``user1``:
$ http --session=user1 -a user1:password example.org X-Foo:Bar
Now you can refer to the session by its name:
Now you can refer to the session by its name, and the previously used
authorization and HTTP headers will automatically be set:
.. code-block:: bash
@ -854,10 +887,20 @@ To use a session without updating it from the request/response exchange
once it is created, specify the session name via
``--session-read-only=SESSION_NAME`` instead.
Sessions are stored as JSON files in ``~/.httpie/sessions/<host>/<name>.json``
Session data are stored in JSON files in the directory
``~/.httpie/sessions/<host>/<name>.json``
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
**Warning:** All session data, including credentials, cookie data,
and custom headers are stored in plain text.
See also `config`_.
Session files can also be created or edited with a text editor.
.. code-block:: bash
$ httpie session edit example.org user1
See also `Config`_.
======
@ -868,7 +911,7 @@ HTTPie uses a simple configuration file that contains a JSON object with the
following keys:
========================= =================================================
``__version__`` HTTPie automatically sets this to its version.
``__meta__`` HTTPie automatically stores some metadata here.
Do not change.
``implicit_content_type`` A ``String`` specifying the implicit content type
@ -878,10 +921,24 @@ following keys:
``default_options`` An ``Array`` (by default empty) of options
that should be applied to every request.
For instance, you can use this option to change
the default style and output options:
``"default_options": ["--style=fruity", "--body"]``
Another useful default option is
``"--session=default"`` to make HTTPie always
use `sessions`_.
Default options from config file can be unset
for a particular invocation via
``--no-OPTION`` arguments passed on the
command line (e.g., ``--no-style``
or ``--no-session``).
========================= =================================================
The default location is ``~/.httpie/config.json``
(``%APPDATA%\httpie\config.json`` on Windows).
The default location of the configuration file is ``~/.httpie/config.json``
(or ``%APPDATA%\httpie\config.json`` on Windows).
The config directory location can be changed by setting the
``HTTPIE_CONFIG_DIR`` environment variable.
@ -1005,6 +1062,11 @@ Please run the existing suite of tests before a pull request is submitted:
Don't forget to add yourself to `AUTHORS.rst`_.
=======
Logo
=======
See `claudiatd/httpie-artwork`_
=======
Authors
@ -1013,7 +1075,6 @@ Authors
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
have contributed.
=======
Licence
=======
@ -1027,8 +1088,14 @@ Changelog
*You can click a version name to see a diff with the previous one.*
* `0.3.1`_ (2012-12-18)
* Require Requests < v1.0 due to unsolved compatibility issues.
* `0.5.0-alpha`_
* `0.4.0`_
* Python 3.3 compatibility.
* Requests >= v1.0.4 compatibility.
* Added support for credentials in URL.
* Added ``--no-option`` for every ``--option`` to be config-friendly.
* Mutually exclusive arguments can be specified multiple times. The
last value is used.
* `0.3.0`_ (2012-09-21)
* Allow output redirection on Windows.
* Added configuration file.
@ -1114,6 +1181,7 @@ Changelog
.. _Jakub Roztocil: http://roztocil.name
.. _@jakubroztocil: https://twitter.com/jakubroztocil
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
.. _0.2.0: https://github.com/jkbr/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jkbr/httpie/compare/0.2.0...0.2.1
@ -1122,7 +1190,9 @@ Changelog
.. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
.. _0.3.0: https://github.com/jkbr/httpie/compare/0.2.7...0.3.0
.. _0.3.1: https://github.com/jkbr/httpie/compare/0.2.7...0.3.1
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
.. _0.5.0-alpha: https://github.com/jkbr/httpie/compare/0.4.0...master
.. _stable version: https://github.com/jkbr/httpie/tree/0.3.0#readme
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE

View File

@ -1,13 +1,14 @@
"""
HTTPie - cURL for humans.
HTTPie - a CLI, cURL-like tool for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.3.0'
__version__ = '0.4.0'
__licence__ = 'BSD'
class exit:
class ExitStatus:
"""Exit status code constants."""
OK = 0
ERROR = 1
ERROR_TIMEOUT = 2

View File

@ -7,17 +7,16 @@ TODO: make the options config friendly, i.e., no mutually exclusive groups to
"""
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
from requests.compat import is_windows
from . import __doc__
from . import __version__
from .sessions import DEFAULT_SESSIONS_DIR
from .compat import is_windows
from .sessions import DEFAULT_SESSIONS_DIR, Session
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS,
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY)
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
def _(text):
@ -27,14 +26,13 @@ def _(text):
parser = Parser(
description='%s <http://httpie.org>' % __doc__.strip(),
epilog=_('''
Suggestions and bug reports are greatly appreciated:
https://github.com/jkbr/httpie/issues
''')
epilog='For every --option there is a --no-option'
' that reverts the option to its default value.\n\n'
'Suggestions and bug reports are greatly appreciated:\n'
'https://github.com/jkbr/httpie/issues'
)
###############################################################################
# Positional arguments.
###############################################################################
@ -43,8 +41,8 @@ positional = parser.add_argument_group(
title='Positional arguments',
description=_('''
These arguments come after any flags and in the
order they are listed here. Only URL is required.'''
)
order they are listed here. Only URL is required.
''')
)
positional.add_argument(
'method', metavar='METHOD',
@ -89,7 +87,7 @@ positional.add_argument(
content_type = parser.add_argument_group(
title='Predefined content types',
description=None
).add_mutually_exclusive_group(required=False)
)
content_type.add_argument(
'--json', '-j', action='store_true',
@ -121,7 +119,7 @@ output_processing = parser.add_argument_group(title='Output processing')
output_processing.add_argument(
'--output', '-o', type=FileType('w+b'),
metavar='FILE',
help= SUPPRESS if not is_windows else _(
help=SUPPRESS if not is_windows else _(
'''
Save output to FILE.
This option is a replacement for piping output to FILE,
@ -153,15 +151,13 @@ output_processing.add_argument(
)
###############################################################################
# Output options
###############################################################################
output_options = parser.add_argument_group(title='Output options')
output_print = output_options.add_mutually_exclusive_group(required=False)
output_print.add_argument('--print', '-p', dest='output_options',
metavar='WHAT',
output_options.add_argument(
'--print', '-p', dest='output_options', metavar='WHAT',
help=_('''
String specifying what the output should contain:
"{request_headers}" stands for the request headers, and
@ -172,14 +168,12 @@ output_print.add_argument('--print', '-p', dest='output_options',
headers and body is printed), if standard output is not redirected.
If the output is piped to another program or to a file,
then only the body is printed by default.
'''.format(
request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,
))
'''.format(request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,))
)
output_print.add_argument(
output_options.add_argument(
'--verbose', '-v', dest='output_options',
action='store_const', const=''.join(OUTPUT_OPTIONS),
help=_('''
@ -187,7 +181,7 @@ output_print.add_argument(
Shortcut for --print={0}.
'''.format(''.join(OUTPUT_OPTIONS)))
)
output_print.add_argument(
output_options.add_argument(
'--headers', '-h', dest='output_options',
action='store_const', const=OUT_RESP_HEAD,
help=_('''
@ -195,7 +189,7 @@ output_print.add_argument(
Shortcut for --print={0}.
'''.format(OUT_RESP_HEAD))
)
output_print.add_argument(
output_options.add_argument(
'--body', '-b', dest='output_options',
action='store_const', const=OUT_RESP_BODY,
help=_('''
@ -204,7 +198,8 @@ output_print.add_argument(
'''.format(OUT_RESP_BODY))
)
output_options.add_argument('--stream', '-S', action='store_true', default=False,
output_options.add_argument(
'--stream', '-S', action='store_true', default=False,
help=_('''
Always stream the output by line, i.e., behave like `tail -f'.
@ -217,8 +212,8 @@ output_options.add_argument('--stream', '-S', action='store_true', default=False
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
'''
))
''')
)
###############################################################################
@ -228,10 +223,13 @@ sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False)
sessions.add_argument(
'--session', metavar='SESSION_NAME',
'--session', metavar='SESSION_NAME', type=RegexValidator(
Session.VALID_NAME_PATTERN,
'Session name contains invalid characters.'
),
help=_('''
Create, or reuse and update a session.
Withing a session, custom headers, auth credential, as well as any
Within a session, custom headers, auth credential, as well as any
cookies sent by the server persist between requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR)
@ -268,7 +266,6 @@ auth.add_argument(
)
# Network
#############################################
@ -339,7 +336,8 @@ troubleshooting.add_argument(
action='help', default=SUPPRESS,
help='Show this help message and exit'
)
troubleshooting.add_argument('--version', action='version', version=__version__)
troubleshooting.add_argument(
'--version', action='version', version=__version__)
troubleshooting.add_argument(
'--traceback', action='store_true', default=False,
help='Prints exception traceback should one occur.'

View File

@ -4,7 +4,6 @@ from pprint import pformat
import requests
import requests.auth
from requests.defaults import defaults
from . import sessions
from . import __version__
@ -21,41 +20,51 @@ def get_response(args, config_dir):
requests_kwargs = get_requests_kwargs(args)
if args.debug:
sys.stderr.write(
'\n>>> requests.request(%s)\n\n' % pformat(requests_kwargs))
sys.stderr.write('\n>>> requests.request(%s)\n\n'
% pformat(requests_kwargs))
if not args.session and not args.session_read_only:
return requests.request(**requests_kwargs)
response = requests.request(**requests_kwargs)
else:
return sessions.get_response(
response = sessions.get_response(
config_dir=config_dir,
name=args.session or args.session_read_only,
request_kwargs=requests_kwargs,
read_only=bool(args.session_read_only),
)
return response
def get_requests_kwargs(args):
"""Translate our `args` into `requests.request` keyword arguments."""
base_headers = defaults['base_headers'].copy()
base_headers['User-Agent'] = DEFAULT_UA
implicit_headers = {
'User-Agent': DEFAULT_UA
}
auto_json = args.data and not args.form
if args.json or auto_json:
base_headers['Accept'] = 'application/json'
implicit_headers['Accept'] = 'application/json'
if args.data:
base_headers['Content-Type'] = JSON
implicit_headers['Content-Type'] = JSON
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
if args.data:
args.data = json.dumps(args.data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
args.data = ''
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
base_headers['Content-Type'] = FORM
implicit_headers['Content-Type'] = FORM
for name, value in implicit_headers.items():
if name not in args.headers:
args.headers[name] = value
credentials = None
if args.auth:
@ -65,7 +74,7 @@ def get_requests_kwargs(args):
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'prefetch': False,
'stream': True,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
@ -80,9 +89,6 @@ def get_requests_kwargs(args):
'files': args.files,
'allow_redirects': args.follow,
'params': args.params,
'config': {
'base_headers': base_headers
}
}
return kwargs

18
httpie/compat.py Normal file
View File

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

View File

@ -3,7 +3,7 @@ import json
import errno
from . import __version__
from requests.compat import is_windows
from .compat import is_windows
DEFAULT_CONFIG_DIR = os.environ.get(
@ -17,7 +17,9 @@ class BaseConfigDict(dict):
name = None
help = None
directory=DEFAULT_CONFIG_DIR
about = None
directory = DEFAULT_CONFIG_DIR
def __init__(self, directory=None, *args, **kwargs):
super(BaseConfigDict, self).__init__(*args, **kwargs)
@ -56,7 +58,15 @@ class BaseConfigDict(dict):
raise
def save(self):
self['__version__'] = __version__
self['__meta__'] = {
'httpie': __version__
}
if self.help:
self['__meta__']['help'] = self.help
if self.about:
self['__meta__']['about'] = self.about
with open(self.path, 'w') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write('\n')
@ -72,6 +82,8 @@ class BaseConfigDict(dict):
class Config(BaseConfigDict):
name = 'config'
help = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file'
DEFAULTS = {
'implicit_content_type': 'json',

View File

@ -14,31 +14,31 @@ import sys
import errno
import requests
from requests.compat import str, is_py3
from httpie import __version__ as httpie_version
from requests import __version__ as requests_version
from pygments import __version__ as pygments_version
from .cli import parser
from .compat import str, is_py3
from .client import get_response
from .models import Environment
from .output import output_stream, write, write_with_colors_win_p3k
from . import exit
from .output import build_output_stream, write, write_with_colors_win_p3k
from . import ExitStatus
def get_exist_status(code, follow=False):
"""Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not follow:
def get_exit_status(http_status, follow=False):
"""Translate HTTP status code to exit status code."""
if 300 <= http_status <= 399 and not follow:
# Redirect
return exit.ERROR_HTTP_3XX
elif 400 <= code <= 499:
return ExitStatus.ERROR_HTTP_3XX
elif 400 <= http_status <= 499:
# Client Error
return exit.ERROR_HTTP_4XX
elif 500 <= code <= 599:
return ExitStatus.ERROR_HTTP_4XX
elif 500 <= http_status <= 599:
# Server Error
return exit.ERROR_HTTP_5XX
return ExitStatus.ERROR_HTTP_5XX
else:
return exit.OK
return ExitStatus.OK
def print_debug_info(env):
@ -54,24 +54,25 @@ def print_debug_info(env):
def main(args=sys.argv[1:], env=Environment()):
"""Run the main program and write the output to ``env.stdout``.
Return exit status.
Return exit status code.
"""
if env.config.default_options:
args = env.config.default_options + args
def error(msg, *args):
def error(msg, *args, **kwargs):
msg = msg % args
env.stderr.write('\nhttp: error: %s\n' % msg)
level = kwargs.get('level', 'error')
env.stderr.write('http: %s: %s\n' % (level, msg))
debug = '--debug' in args
traceback = debug or '--traceback' in args
status = exit.OK
exit_status = ExitStatus.OK
if debug:
print_debug_info(env)
if args == ['--debug']:
sys.exit(exit.OK)
return exit_status
try:
args = parser.parse_args(args=args, env=env)
@ -79,15 +80,18 @@ def main(args=sys.argv[1:], env=Environment()):
response = get_response(args, config_dir=env.config.directory)
if args.check_status:
status = get_exist_status(response.status_code,
args.follow)
if status and not env.stdout_isatty:
error('%s %s', response.raw.status, response.raw.reason)
exit_status = get_exit_status(response.status_code, args.follow)
stream = output_stream(args, env, response.request, response)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
error('HTTP %s %s',
response.raw.status,
response.raw.reason,
level='warning')
write_kwargs = {
'stream': stream,
'stream': build_output_stream(args, env,
response.request,
response),
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
@ -108,16 +112,18 @@ def main(args=sys.argv[1:], env=Environment()):
if traceback:
raise
env.stderr.write('\n')
status = exit.ERROR
exit_status = ExitStatus.ERROR
except requests.Timeout:
status = exit.ERROR_TIMEOUT
exit_status = ExitStatus.ERROR_TIMEOUT
error('Request timed out (%ss).', args.timeout)
except Exception as e:
# TODO: distinguish between expected and unexpected errors.
# network errors vs. bugs, etc.
# TODO: Better distinction between expected and unexpected errors.
# Network errors vs. bugs, etc.
if traceback:
raise
error('%s: %s', type(e).__name__, str(e))
status = exit.ERROR
exit_status = ExitStatus.ERROR
return status
return exit_status

View File

@ -8,15 +8,18 @@ import json
import mimetypes
import getpass
from io import BytesIO
from argparse import ArgumentParser, ArgumentTypeError
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
# TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jkbr/httpie/issues/130
from requests.structures import CaseInsensitiveDict
from requests.compat import str, urlparse
from .compat import urlsplit, str
HTTP_POST = 'POST'
@ -96,7 +99,10 @@ class Parser(ArgumentParser):
self.env = env
args = super(Parser, self).parse_args(args, namespace)
args, no_options = super(Parser, self).parse_known_args(args,
namespace)
self._apply_no_options(args, no_options)
if not args.json and env.config.implicit_content_type == 'form':
args.form = True
@ -120,12 +126,54 @@ class Parser(ArgumentParser):
scheme = HTTPS if env.progname == 'https' else HTTP
args.url = scheme + args.url
if args.auth and not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(urlparse(args.url).netloc)
self._process_auth(args)
return args
def _process_auth(self, args):
url = urlsplit(args.url)
if args.auth:
if not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(url.netloc)
elif url.username is not None:
# Handle http://username:password@hostname/
username, password = url.username, url.password
args.auth = AuthCredentials(
key=username,
value=password,
sep=SEP_CREDENTIALS,
orig=SEP_CREDENTIALS.join([username, password])
)
def _apply_no_options(self, args, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g.,
specified in config.
"""
invalid = []
for option in no_options:
if not option.startswith('--no-'):
invalid.append(option)
continue
# --no-option => --option
inverted = '--' + option[5:]
for action in self._actions:
if inverted in action.option_strings:
setattr(args, action.dest, action.default)
break
else:
invalid.append(option)
if invalid:
msg = 'unrecognized arguments: %s'
self.error(msg % ' '.join(invalid))
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
@ -262,6 +310,38 @@ class KeyValue(object):
return self.__dict__ == other.__dict__
def session_name_arg_type(name):
from .sessions import Session
if not Session.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in session names: "%s"'
% name)
return name
def host_name_arg_type(name):
from .sessions import Host
if not Host.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in host names: "%s"'
% name)
return name
class RegexValidator(object):
def __init__(self, pattern, error_message):
self.pattern = re.compile(pattern)
self.error_message = error_message
def __call__(self, value):
if not self.pattern.search(value):
raise ArgumentError(None, self.error_message)
return value
class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`.
@ -403,9 +483,6 @@ class ParamDict(OrderedDict):
data and URL params.
"""
# NOTE: Won't work when used for form data with multiple values
# for a field and a file field is present:
# https://github.com/kennethreitz/requests/issues/737
if key not in self:
super(ParamDict, self).__setitem__(key, value)
else:

View File

@ -1,30 +0,0 @@
"""
Provides the `httpie' management command.
Note that the main `http' command points to `httpie.__main__.main()`.
"""
import argparse
from . import sessions
from . import __version__
parser = argparse.ArgumentParser(
description='The HTTPie management command.',
version=__version__
)
subparsers = parser.add_subparsers()
# Only sessions as of now.
sessions.add_commands(subparsers)
def main():
args = parser.parse_args()
args.command(args)
if __name__ == '__main__':
main()

View File

@ -1,8 +1,8 @@
import os
import sys
from requests.compat import urlparse, is_windows, bytes, str
from .config import DEFAULT_CONFIG_DIR, Config
from .compat import urlsplit, is_windows, bytes, str
class Environment(object):
@ -13,7 +13,6 @@ class Environment(object):
"""
#noinspection PyUnresolvedReferences
is_windows = is_windows
progname = os.path.basename(sys.argv[0])
@ -143,33 +142,18 @@ class HTTPRequest(HTTPMessage):
@property
def headers(self):
"""Return Request-Line"""
url = urlparse(self._orig.url)
url = urlsplit(self._orig.url)
# Querystring
qs = ''
if url.query or self._orig.params:
qs = '?'
if url.query:
qs += url.query
# Requests doesn't make params part of ``request.url``.
if self._orig.params:
if url.query:
qs += '&'
#noinspection PyUnresolvedReferences
qs += type(self._orig)._encode_params(self._orig.params)
# Request-Line
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method,
path=url.path or '/',
query=qs
query='?' + url.query if url.query else ''
)
headers = dict(self._orig.headers)
if 'Host' not in headers:
headers['Host'] = urlparse(self._orig.url).netloc
headers['Host'] = url.netloc
headers = ['%s: %s' % (name, value)
for name, value in headers.items()]
@ -184,26 +168,8 @@ class HTTPRequest(HTTPMessage):
@property
def body(self):
"""Reconstruct and return the original request body bytes."""
if self._orig.files:
# TODO: would be nice if we didn't need to encode the files again
# FIXME: Also the boundary header doesn't match the one used.
for fn, fd in self._orig.files.values():
# Rewind the files as they have already been read before.
fd.seek(0)
body, _ = self._orig._encode_files(self._orig.files)
else:
try:
body = self._orig.data
except AttributeError:
# requests < 0.12.1
body = self._orig._enc_data
if isinstance(body, dict):
#noinspection PyUnresolvedReferences
body = type(self._orig)._encode_params(body)
if isinstance(body, str):
body = body.encode('utf8')
return body
body = self._orig.body
if isinstance(body, str):
# Happens with JSON/form request data parsed from the command line.
body = body.encode('utf8')
return body or b''

View File

@ -12,8 +12,8 @@ from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound
from requests.compat import is_windows
from .compat import is_windows
from .solarized import Solarized256Style
from .models import HTTPRequest, HTTPResponse, Environment
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
@ -78,23 +78,21 @@ def write_with_colors_win_p3k(stream, outfile, flush):
outfile.flush()
def output_stream(args, env, request, response):
def build_output_stream(args, env, request, response):
"""Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks.
"""
Stream = make_stream(env, args)
req_h = OUT_REQ_HEAD in args.output_options
req_b = OUT_REQ_BODY in args.output_options
resp_h = OUT_RESP_HEAD in args.output_options
resp_b = OUT_RESP_BODY in args.output_options
req = req_h or req_b
resp = resp_h or resp_b
output = []
Stream = get_stream_type(env, args)
if req:
output.append(Stream(
@ -120,7 +118,7 @@ def output_stream(args, env, request, response):
return chain(*output)
def make_stream(env, args):
def get_stream_type(env, args):
"""Pick the right stream type based on `env` and `args`.
Wrap it in a partial with the type-specific args so that
we don't need to think what stream we are dealing with.
@ -147,7 +145,7 @@ def make_stream(env, args):
class BaseStream(object):
"""Base HTTP message stream class."""
"""Base HTTP message output stream class."""
def __init__(self, msg, with_headers=True, with_body=True):
"""
@ -156,27 +154,28 @@ class BaseStream(object):
:param with_body: if `True`, body will be included
"""
assert with_headers or with_body
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
def _headers(self):
def _get_headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('ascii')
def _body(self):
def _iter_body(self):
"""Return an iterator over the message body."""
raise NotImplementedError()
def __iter__(self):
"""Return an iterator over `self.msg`."""
if self.with_headers:
yield self._headers()
yield self._get_headers()
yield b'\r\n\r\n'
if self.with_body:
try:
for chunk in self._body():
for chunk in self._iter_body():
yield chunk
except BinarySuppressedError as e:
if self.with_headers:
@ -194,7 +193,7 @@ class RawStream(BaseStream):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def _body(self):
def _iter_body(self):
return self.msg.iter_body(self.chunk_size)
@ -222,7 +221,7 @@ class EncodedStream(BaseStream):
# Default to utf8 when unsure.
self.output_encoding = output_encoding or 'utf8'
def _body(self):
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
@ -248,11 +247,11 @@ class PrettyStream(EncodedStream):
super(PrettyStream, self).__init__(**kwargs)
self.processor = processor
def _headers(self):
def _get_headers(self):
return self.processor.process_headers(
self.msg.headers).encode(self.output_encoding)
def _body(self):
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
@ -276,9 +275,8 @@ class BufferedPrettyStream(PrettyStream):
CHUNK_SIZE = 1024 * 10
def _body(self):
def _iter_body(self):
#noinspection PyArgumentList
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
body = bytearray()

View File

@ -1,22 +1,18 @@
"""Persistent, JSON-serialized sessions.
"""
import re
import os
import sys
import glob
import errno
import codecs
import shutil
import subprocess
import requests
from requests.compat import urlparse
from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from argparse import OPTIONAL
from .compat import urlsplit
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
from .output import PygmentsProcessor
SESSIONS_DIR_NAME = 'sessions'
@ -32,9 +28,8 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
host = Host(
root_dir=sessions_dir,
name=request_kwargs['headers'].get('Host', None)
or urlparse(request_kwargs['url']).netloc.split('@')[-1]
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
)
session = Session(host, name)
session.load()
@ -49,15 +44,17 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
elif session.auth:
request_kwargs['auth'] = session.auth
rsession = requests.Session(cookies=session.cookies)
requests_session = requests.Session()
requests_session.cookies = session.cookies
try:
response = rsession.request(**request_kwargs)
response = requests_session.request(**request_kwargs)
except Exception:
raise
else:
# Existing sessions with `read_only=True` don't get updated.
if session.is_new or not read_only:
session.cookies = rsession.cookies
session.cookies = requests_session.cookies
session.save()
return response
@ -65,24 +62,46 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
class Host(object):
"""A host is a per-host directory on the disk containing sessions files."""
def __init__(self, name, root_dir=DEFAULT_CONFIG_DIR):
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 a iterator yielding `(session_name, session_path)`."""
"""Return an iterator yielding `Session` instances."""
for fn in sorted(glob.glob1(self.path, '*.json')):
yield os.path.splitext(fn)[0], os.path.join(self.path, fn)
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):
# Name will include ':' if a port is specified, which is invalid
# on windows. DNS does not allow '_' in a domain, or for it to end
# in a number (I think?)
path = os.path.join(self.root_dir, self.name.replace(':', '_'))
path = os.path.join(self.root_dir, self._quote_name(self.name))
try:
os.makedirs(path, mode=0o700)
except OSError as e:
@ -90,28 +109,35 @@ class Host(object):
raise
return path
@classmethod
def all(cls):
"""Return a generator yielding a host at a time."""
for name in sorted(glob.glob1(DEFAULT_SESSIONS_DIR, '*')):
if os.path.isdir(os.path.join(DEFAULT_SESSIONS_DIR, name)):
yield Host(name)
class Session(BaseConfigDict):
""""""
help = 'https://github.com/jkbr/httpie#sessions'
about = 'HTTPie session file'
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
def __init__(self, host, name, *args, **kwargs):
assert self.VALID_NAME_PATTERN.match(name)
super(Session, self).__init__(*args, **kwargs)
self.host = host
self.name = name
self['headers'] = {}
self['cookies'] = {}
self['auth'] = {
'type': None,
'username': None,
'password': None
}
@property
def directory(self):
return self.host.path
@property
def verbose_name(self):
return '%s %s %s' % (self.host.name, self.name, self.path)
@property
def cookies(self):
jar = RequestsCookieJar()
@ -123,26 +149,22 @@ class Session(BaseConfigDict):
@cookies.setter
def cookies(self, jar):
excluded = [
'_rest', 'name', 'port_specified',
'domain_specified', 'domain_initial_dot',
'path_specified', 'comment', 'comment_url'
]
# http://docs.python.org/2/library/cookielib.html#cookie-objects
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for host in jar._cookies.values():
for path in host.values():
for name, cookie in path.items():
cookie_dict = {}
for k, v in cookie.__dict__.items():
if k not in excluded:
cookie_dict[k] = v
self['cookies'][name] = cookie_dict
self['cookies'][name] = dict(
(attname, getattr(cookie, attname))
for attname in stored_attrs
)
@property
def auth(self):
auth = self.get('auth', None)
if not auth:
return None
if not auth or not auth['type']:
return
Auth = {'basic': HTTPBasicAuth,
'digest': HTTPDigestAuth}[auth['type']]
return Auth(auth['username'], auth['password'])
@ -155,79 +177,3 @@ class Session(BaseConfigDict):
'username': cred.username,
'password': cred.password,
}
# The commands are disabled for now.
# TODO: write tests for the commands.
def list_command(args):
if args.host:
for name, path in Host(args.host):
print(name + ' [' + path + ']')
else:
for host in Host.all():
print(host.name)
for name, path in host:
print(' ' + name + ' [' + path + ']')
def show_command(args):
path = Session(Host(args.host), args.name).path
if not os.path.exists(path):
sys.stderr.write('Session "%s" does not exist [%s].\n'
% (args.name, path))
sys.exit(1)
with codecs.open(path, encoding='utf8') as f:
print(path + ':\n')
proc = PygmentsProcessor()
print(proc.process_body(f.read(), 'application/json', 'json'))
print('')
def delete_command(args):
host = Host(args.host)
if not args.name:
host.delete()
else:
Session(host, args.name).delete()
def edit_command(args):
editor = os.environ.get('EDITOR', None)
if not editor:
sys.stderr.write(
'You need to configure the environment variable EDITOR.\n')
sys.exit(1)
command = editor.split()
command.append(Session(Host(args.host), args.name).path)
subprocess.call(command)
def add_commands(subparsers):
# List
list_ = subparsers.add_parser('session-list', help='list sessions')
list_.set_defaults(command=list_command)
list_.add_argument('host', nargs=OPTIONAL)
# Show
show = subparsers.add_parser('session-show', help='show a session')
show.set_defaults(command=show_command)
show.add_argument('host')
show.add_argument('name')
# Edit
edit = subparsers.add_parser(
'session-edit', help='edit a session in $EDITOR')
edit.set_defaults(command=edit_command)
edit.add_argument('host')
edit.add_argument('name')
# Delete
delete = subparsers.add_parser('session-delete', help='delete a session')
delete.set_defaults(command=delete_command)
delete.add_argument('host')
delete.add_argument('name', nargs=OPTIONAL,
help='The name of the session to be deleted.'
' If not specified, all host sessions are deleted.')

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
#

View File

@ -12,8 +12,7 @@ if sys.argv[-1] == 'test':
requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'requests>=0.10.1,<1.0',
'requests>=1.0.4',
'Pygments>=1.5'
]
if sys.version_info[:2] in ((2, 6), (3, 1)):
@ -47,8 +46,7 @@ setup(
entry_points={
'console_scripts': [
'http = httpie.__main__:main',
# Not ready yet.
# 'httpie = httpie.manage:main',
'httpie = httpie.manage:main',
],
},
install_requires=requirements,

View File

@ -19,7 +19,6 @@ To make it run faster and offline you can::
HTTPBIN_URL=http://localhost:5000 tox
"""
from functools import partial
import subprocess
import os
import sys
@ -29,7 +28,6 @@ import tempfile
import unittest
import shutil
from requests.compat import urlparse
try:
from urllib.request import urlopen
except ImportError:
@ -38,6 +36,7 @@ try:
from unittest import skipIf, skip
except ImportError:
skip = lambda msg: lambda self: None
def skipIf(cond, reason):
def decorator(test_method):
if cond:
@ -46,7 +45,6 @@ except ImportError:
return decorator
from requests import __version__ as requests_version
from requests.compat import is_windows, is_py26, bytes, str
#################################################################
@ -57,12 +55,13 @@ from requests.compat import is_windows, is_py26, bytes, str
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import exit
from httpie import ExitStatus
from httpie import input
from httpie.models import Environment
from httpie.core import main
from httpie.output import BINARY_SUPPRESSED_NOTICE
from httpie.input import ParseError
from httpie.compat import is_windows, is_py26, bytes, str
CRLF = '\r\n'
@ -132,6 +131,7 @@ class TestEnvironment(Environment):
if self.delete_config_dir:
self._shutil.rmtree(self.config_dir)
def has_docutils():
try:
#noinspection PyUnresolvedReferences
@ -140,6 +140,7 @@ def has_docutils():
except ImportError:
return False
def get_readme_errors():
p = subprocess.Popen([
'rst2pseudoxml.py',
@ -154,6 +155,8 @@ def get_readme_errors():
class BytesResponse(bytes):
stderr = json = exit_status = None
class StrResponse(str):
stderr = json = exit_status = None
@ -185,7 +188,7 @@ def http(*args, **kwargs):
sys.stderr.write(env.stderr.read())
raise
except SystemExit:
exit_status = exit.ERROR
exit_status = ExitStatus.ERROR
env.stdout.seek(0)
env.stderr.seek(0)
@ -224,6 +227,8 @@ def http(*args, **kwargs):
class BaseTestCase(unittest.TestCase):
maxDiff = 100000
if is_py26:
def assertIn(self, member, container, msg=None):
self.assertTrue(member in container, msg)
@ -858,6 +863,31 @@ class AuthTest(BaseTestCase):
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url(self):
url = httpbin('/basic-auth/user/password')
url = 'http://user:password@' + url.split('http://', 1)[1]
r = http(
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url_auth_flag_has_priority(self):
"""When credentials are passed in URL and via -a at the same time,
then the ones from -a are used."""
url = httpbin('/basic-auth/user/password')
url = 'http://user:wrong_password@' + url.split('http://', 1)[1]
r = http(
'--auth=user:password',
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
class ExitStatusTest(BaseTestCase):
@ -867,7 +897,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/200')
)
self.assertIn(OK, r)
self.assertEqual(r.exit_status, exit.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
def test_error_response_exits_0_without_check_status(self):
r = http(
@ -875,7 +905,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, exit.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
self.assertTrue(not r.stderr)
def test_timeout_exit_status(self):
@ -884,7 +914,7 @@ class ExitStatusTest(BaseTestCase):
'GET',
httpbin('/delay/1')
)
self.assertEqual(r.exit_status, exit.ERROR_TIMEOUT)
self.assertEqual(r.exit_status, ExitStatus.ERROR_TIMEOUT)
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
r = http(
@ -895,7 +925,7 @@ class ExitStatusTest(BaseTestCase):
env=TestEnvironment(stdout_isatty=False,)
)
self.assertIn('HTTP/1.1 301', r)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_3XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_3XX)
self.assertIn('301 moved permanently', r.stderr.lower())
@skipIf(requests_version == '0.13.6',
@ -909,7 +939,7 @@ class ExitStatusTest(BaseTestCase):
)
# The redirect will be followed so 200 is expected.
self.assertIn('HTTP/1.1 200 OK', r)
self.assertEqual(r.exit_status, exit.OK)
self.assertEqual(r.exit_status, ExitStatus.OK)
def test_4xx_check_status_exits_4(self):
r = http(
@ -918,7 +948,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/401')
)
self.assertIn('HTTP/1.1 401', r)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_4XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_4XX)
# Also stderr should be empty since stdout isn't redirected.
self.assertTrue(not r.stderr)
@ -929,7 +959,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_5XX)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_5XX)
class WindowsOnlyTests(BaseTestCase):
@ -982,7 +1012,8 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK_COLOR, r)
def test_encoded_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--pretty=none',
@ -1000,7 +1031,8 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK, r)
def test_redirected_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
"""Test that --stream works with non-prettified
redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--pretty=none',
@ -1024,7 +1056,6 @@ class LineEndingsTest(BaseTestCase):
as the headers/body separator."""
def _validate_crlf(self, msg):
#noinspection PyUnresolvedReferences
lines = iter(msg.splitlines(True))
for header in lines:
if header == CRLF:
@ -1059,7 +1090,7 @@ class LineEndingsTest(BaseTestCase):
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status,0)
self.assertEqual(r.exit_status, 0)
self._validate_crlf(r)
def test_CRLF_ugly_request(self):
@ -1231,11 +1262,33 @@ class ArgumentParserTestCase(unittest.TestCase):
self.assertEqual(args.items, [
input.KeyValue(
key='new_item', value='a', sep='=', orig='new_item=a'),
input.KeyValue(key
='old_item', value='b', sep='=', orig='old_item=b'),
input.KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b'),
])
class TestNoOptions(BaseTestCase):
def test_valid_no_options(self):
r = http(
'--verbose',
'--no-verbose',
'GET',
httpbin('/get')
)
self.assertNotIn('GET /get HTTP/1.1', r)
def test_invalid_no_options(self):
r = http(
'--no-war',
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status, 1)
self.assertIn('unrecognized arguments: --no-war', r.stderr)
self.assertNotIn('GET /get HTTP/1.1', r)
class READMETest(BaseTestCase):
@skipIf(not has_docutils(), 'docutils not installed')
@ -1350,10 +1403,12 @@ class SessionTest(BaseTestCase):
)
self.assertIn(OK, r3)
# Origin can differ on Travis.
del r1.json['origin'], r3.json['origin']
# Should be the same as before r2.
self.assertDictEqual(r1.json, r3.json)
if __name__ == '__main__':
#noinspection PyCallingNonCallable
unittest.main()

View File

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