Added initial support for persistent sessions.

This commit is contained in:
Jakub Roztocil 2012-08-17 23:29:23 +02:00
commit 4c0d7d526f
9 changed files with 247 additions and 126 deletions

View File

@ -502,6 +502,30 @@ path. The path can also be configured via the environment variable
``REQUESTS_CA_BUNDLE``. ``REQUESTS_CA_BUNDLE``.
========
Sessions
========
HTTPie supports named sessions, where several options and cookies sent
by the server persists between requests:
.. code-block:: bash
http --session=user1 --auth=user1:password example.org
Now you can always refer to the session by passing ``--session=user1``,
and the credentials and cookies will be reused:
http --session=user1 GET example.org
Since sessions are named, you can switch between multiple sessions:
http --session=user2 --auth=user2:password example.org
Note that session cookies respect domain and path.
Session data are store in ``~/httpie/sessions/<name>.pickle``.
============== ==============
Output Options Output Options
============== ==============
@ -700,12 +724,14 @@ Also, the following formatting is applied:
One of these options can be used to control output processing: One of these options can be used to control output processing:
=============== ============================================================== ==================== ========================================================
``--pretty`` Apply both colors and formatting. Default for terminal output. ``--pretty=all`` Apply both colors and formatting.
``--colors`` Apply colors. Default for terminal output.
``--format`` Apply formatting. ``--pretty=colors`` Apply colors.
``--ugly, -u`` Disables output processing. Default for redirected output. ``--pretty=format`` Apply formatting.
=============== ============================================================== ``--pretty=none`` Disables output processing.
Default for redirected output.
==================== ========================================================
----------- -----------
Binary data Binary data
@ -743,8 +769,7 @@ Redirected Output
HTTPie uses **different defaults** for redirected output than for HTTPie uses **different defaults** for redirected output than for
`terminal output`_: `terminal output`_:
* Formatting and colors aren't applied (unless ``--pretty``, ``--format``, * Formatting and colors aren't applied (unless ``--pretty`` is specified).
or ``--colors`` is set).
* Only the response body is printed (unless one of the `output options`_ is set). * Only the response body is printed (unless one of the `output options`_ is set).
* Also, binary data isn't suppressed. * Also, binary data isn't suppressed.
@ -771,7 +796,7 @@ Force colorizing and formatting, and show both the request and the response in
.. code-block:: bash .. code-block:: bash
$ http --pretty --verbose example.org | less -R $ http --pretty=all --verbose example.org | less -R
The ``-R`` flag tells ``less`` to interpret color escape sequences included The ``-R`` flag tells ``less`` to interpret color escape sequences included
@ -880,7 +905,7 @@ and that only a small portion of the command is used to control HTTPie and
doesn't directly correspond to any part of the request (here it's only ``-f`` doesn't directly correspond to any part of the request (here it's only ``-f``
asking HTTPie to send a form request). asking HTTPie to send a form request).
The two modes, ``--pretty`` (default for terminal) and ``--ugly, -u`` The two modes, ``--pretty=all`` (default for terminal) and ``--pretty=none``
(default for redirected output), allow for both user-friendly interactive use (default for redirected output), allow for both user-friendly interactive use
and usage from scripts, where HTTPie serves as a generic HTTP client. and usage from scripts, where HTTPie serves as a generic HTTP client.
@ -956,20 +981,22 @@ 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.2.8dev`_ * `0.2.8-alpha`_
* Fixed colorized output on Windows with Python 3. * Added persistent session support.
* Fixed installation on Windows with Python 3. * Fixed installation on Windows with Python 3.
* Fixed colorized output on Windows with Python 3.
* CRLF HTTP header field separation in the output. * CRLF HTTP header field separation in the output.
* Added exit status code ``2`` for timed-out requests. * Added exit status code ``2`` for timed-out requests.
* Added ``--colors`` and ``--format`` in addition to ``--pretty``, to * Added the option to separate colorizing and formatting
be able to separate colorizing and formatting. (``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``).
``--ugly`` has bee removed in favor of ``--pretty=none``.
* `0.2.7`_ (2012-08-07) * `0.2.7`_ (2012-08-07)
* Compatibility with Requests 0.13.6. * Compatibility with Requests 0.13.6.
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable * Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
streaming also with ``--pretty`` and to ensure a more frequent output streaming also with ``--pretty`` and to ensure a more frequent output
flushing. flushing.
* Support for efficient large file downloads. * Support for efficient large file downloads.
* Sort headers by name (unless ``--ugly``). * Sort headers by name (unless ``--pretty=none``).
* Response body is fetched only when needed (e.g., not with ``--headers``). * Response body is fetched only when needed (e.g., not with ``--headers``).
* Improved content type matching. * Improved content type matching.
* Updated Solarized color scheme. * Updated Solarized color scheme.
@ -1042,7 +1069,7 @@ Changelog
.. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5 .. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5
.. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6 .. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7 .. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
.. _0.2.8dev: https://github.com/jkbr/httpie/compare/0.2.7...master .. _0.2.8-alpha: https://github.com/jkbr/httpie/compare/0.2.7...master
.. _stable version: https://github.com/jkbr/httpie/tree/0.2.7#readme .. _stable version: https://github.com/jkbr/httpie/tree/0.2.7#readme
.. _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 - cURL for humans.
""" """
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.2.8dev' __version__ = '0.2.8-alpha'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -14,8 +14,7 @@ from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS, SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_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,
PRETTY_STDOUT_TTY_ONLY, PRETTY_ALL, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY)
PRETTY_FORMAT, PRETTY_COLORS)
def _(text): def _(text):
@ -69,27 +68,15 @@ parser.add_argument(
) )
) )
prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument( parser.add_argument(
'--pretty', dest='prettify', action='store_const', const=PRETTY_ALL, '--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY,
default=PRETTY_STDOUT_TTY_ONLY, choices=sorted(PRETTY_MAP.keys()),
help=_(''' help=_('''
Apply both colors and formatting. Default for terminal output. Controls output processing. The value can be "none" to not prettify
''') the output (default for redirected output), "all" to apply both colors
) and formatting
prettify.add_argument( (default for terminal output), "colors", or "format".
'--colors', dest='prettify', action='store_const', const=PRETTY_COLORS,
help=_('''Apply colors to the output.''')
)
prettify.add_argument(
'--format', dest='prettify', action='store_const', const=PRETTY_FORMAT,
help=_('''Apply formatting to the output.''')
)
prettify.add_argument(
'--ugly', '-u', dest='prettify', action='store_false',
help=_('''
Disables output processing.
Default for redirected output.
''') ''')
) )
@ -182,9 +169,19 @@ parser.add_argument(
''') ''')
) )
parser.add_argument(
'--session', metavar='NAME',
help=_('''
Create or reuse a session.
Withing a session, values of --auth, --timeout,
--verify, --proxies are persistent, as well as any
cookies sent by the server.
''')
)
# ``requests.request`` keyword arguments. # ``requests.request`` keyword arguments.
parser.add_argument( parser.add_argument(
'--auth', '-a', metavar='USER:PASS', '--auth', '-a', metavar='USER[:PASS]',
type=AuthCredentialsArgType(SEP_CREDENTIALS), type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_(''' help=_('''
username:password. username:password.

77
httpie/client.py Normal file
View File

@ -0,0 +1,77 @@
import json
import sys
from pprint import pformat
import requests
import requests.auth
from .import sessions
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
def get_response(args):
requests_kwargs = get_requests_kwargs(args)
if args.debug:
sys.stderr.write(
'\n>>> requests.request(%s)\n\n' % pformat(requests_kwargs))
if args.session:
return sessions.get_response(args.session, requests_kwargs)
else:
return requests.request(**requests_kwargs)
def get_requests_kwargs(args):
"""Send the request and return a `request.Response`."""
auto_json = args.data and not args.form
if args.json or auto_json:
if 'Content-Type' not in args.headers and args.data:
args.headers['Content-Type'] = JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = FORM
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'prefetch': False,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
'data': args.data,
'verify': {
'yes': True,
'no': False
}.get(args.verify,args.verify),
'timeout': args.timeout,
'auth': credentials,
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.allow_redirects,
'params': args.params
}
return kwargs

6
httpie/config.py Normal file
View File

@ -0,0 +1,6 @@
import os
__author__ = 'jakub'
CONFIG_DIR = os.path.expanduser('~/.httpie')

View File

@ -11,77 +11,21 @@ Invocation flow:
""" """
import sys import sys
import json
import errno import errno
from pprint import pformat
import requests import requests
import requests.auth
from requests.compat import str, is_py3 from requests.compat import str, is_py3
from httpie import __version__ as httpie_version 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 .cli import parser
from .client import get_response
from .models import Environment from .models import Environment
from .output import output_stream, write, write_with_colors_win_p3k from .output import output_stream, write, write_with_colors_win_p3k
from . import EXIT from . import EXIT
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
def get_requests_kwargs(args):
"""Send the request and return a `request.Response`."""
auto_json = args.data and not args.form
if args.json or auto_json:
if 'Content-Type' not in args.headers and args.data:
args.headers['Content-Type'] = JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = FORM
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'prefetch': False,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
'data': args.data,
'verify': {
'yes': True,
'no': False
}.get(args.verify,args.verify),
'timeout': args.timeout,
'auth': credentials,
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.allow_redirects,
'params': args.params
}
return kwargs
def get_exist_status(code, allow_redirects=False): def get_exist_status(code, allow_redirects=False):
"""Translate HTTP status code to exit status.""" """Translate HTTP status code to exit status."""
@ -121,13 +65,7 @@ def main(args=sys.argv[1:], env=Environment()):
try: try:
args = parser.parse_args(args=args, env=env) args = parser.parse_args(args=args, env=env)
requests_kwargs = get_requests_kwargs(args) response = get_response(args)
if args.debug:
sys.stderr.write(
'\n>>> requests.request(%s)\n\n' % pformat(requests_kwargs))
response = requests.request(**requests_kwargs)
if args.check_status: if args.check_status:
status = get_exist_status(response.status_code, status = get_exist_status(response.status_code,

View File

@ -67,9 +67,12 @@ OUTPUT_OPTIONS = frozenset([
]) ])
# Pretty # Pretty
PRETTY_ALL = ['format', 'colors'] PRETTY_MAP = {
PRETTY_FORMAT = ['format'] 'all': ['format', 'colors'],
PRETTY_COLORS = ['colors'] 'colors': ['colors'],
'format': ['format'],
'none': []
}
PRETTY_STDOUT_TTY_ONLY = object() PRETTY_STDOUT_TTY_ONLY = object()
@ -114,6 +117,7 @@ class Parser(argparse.ArgumentParser):
env.stdout_isatty = False env.stdout_isatty = False
self._process_output_options(args, env) self._process_output_options(args, env)
self._process_pretty_options(args, env)
self._guess_method(args, env) self._guess_method(args, env)
self._parse_items(args) self._parse_items(args)
@ -128,10 +132,6 @@ class Parser(argparse.ArgumentParser):
# 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.
args.auth.prompt_password(urlparse(args.url).netloc) args.auth.prompt_password(urlparse(args.url).netloc)
if args.prettify == PRETTY_STDOUT_TTY_ONLY:
args.prettify = PRETTY_ALL if env.stdout_isatty else []
elif args.prettify and env.is_windows:
self.error('Only terminal output can be prettified on Windows.')
return args return args
@ -246,6 +246,14 @@ class Parser(argparse.ArgumentParser):
if unknown: if unknown:
self.error('Unknown output options: %s' % ','.join(unknown)) self.error('Unknown output options: %s' % ','.join(unknown))
def _process_pretty_options(self, args, env):
if args.prettify == PRETTY_STDOUT_TTY_ONLY:
args.prettify = PRETTY_MAP['all' if env.stdout_isatty else 'none']
elif args.prettify and env.is_windows:
self.error('Only terminal output can be prettified on Windows.')
else:
args.prettify = PRETTY_MAP[args.prettify]
class ParseError(Exception): class ParseError(Exception):
pass pass

68
httpie/sessions.py Normal file
View File

@ -0,0 +1,68 @@
import os
import pickle
import errno
from requests import Session
from .config import CONFIG_DIR
SESSIONS_DIR = os.path.join(CONFIG_DIR, 'sessions')
def get_response(name, request_kwargs):
session = load(name)
session_kwargs, request_kwargs = split_kwargs(request_kwargs)
headers = session_kwargs.pop('headers', None)
if headers:
session.headers.update(headers)
session.__dict__.update(session_kwargs)
try:
response = session.request(**request_kwargs)
except Exception:
raise
else:
save(session, name)
return response
def split_kwargs(requests_kwargs):
session = {}
request = {}
session_attrs = [
'auth', 'timeout',
'verify', 'proxies',
'params'
]
for k, v in requests_kwargs.items():
if v is not None:
if k in session_attrs:
session[k] = v
else:
request[k] = v
return session, request
def get_path(name):
try:
os.makedirs(SESSIONS_DIR, mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
return os.path.join(SESSIONS_DIR, name + '.pickle')
def load(name):
try:
with open(get_path(name), 'rb') as f:
return pickle.load(f)
except IOError as e:
if e.errno != errno.ENOENT:
raise
return Session()
def save(session, name):
with open(get_path(name), 'wb') as f:
pickle.dump(session, f)

View File

@ -524,7 +524,7 @@ class ImplicitHTTPMethodTest(BaseTestCase):
class PrettyOptionsTest(BaseTestCase): class PrettyOptionsTest(BaseTestCase):
"""Test the --pretty / --ugly flag handling.""" """Test the --pretty flag handling."""
def test_pretty_enabled_by_default(self): def test_pretty_enabled_by_default(self):
r = http( r = http(
@ -543,7 +543,7 @@ class PrettyOptionsTest(BaseTestCase):
def test_force_pretty(self): def test_force_pretty(self):
r = http( r = http(
'--pretty', '--pretty=all',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=TestEnvironment(stdout_isatty=False, colors=256), env=TestEnvironment(stdout_isatty=False, colors=256),
@ -552,7 +552,7 @@ class PrettyOptionsTest(BaseTestCase):
def test_force_ugly(self): def test_force_ugly(self):
r = http( r = http(
'--ugly', '--pretty=none',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
) )
@ -565,7 +565,7 @@ class PrettyOptionsTest(BaseTestCase):
""" """
r = http( r = http(
'--print=B', '--print=B',
'--pretty', '--pretty=all',
httpbin('/post'), httpbin('/post'),
'Content-Type:text/foo+json', 'Content-Type:text/foo+json',
'a=b', 'a=b',
@ -576,7 +576,7 @@ class PrettyOptionsTest(BaseTestCase):
def test_colors_option(self): def test_colors_option(self):
r = http( r = http(
'--print=B', '--print=B',
'--colors', '--pretty=colors',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
'a=b', 'a=b',
@ -590,7 +590,7 @@ class PrettyOptionsTest(BaseTestCase):
def test_format_option(self): def test_format_option(self):
r = http( r = http(
'--print=B', '--print=B',
'--format', '--pretty=format',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
'a=b', 'a=b',
@ -737,7 +737,7 @@ class BinaryResponseDataTest(BaseTestCase):
def test_binary_suppresses_when_not_terminal_but_pretty(self): def test_binary_suppresses_when_not_terminal_but_pretty(self):
r = http( r = http(
'--pretty', '--pretty=all',
'GET', 'GET',
self.url, self.url,
env=TestEnvironment(stdin_isatty=True, env=TestEnvironment(stdin_isatty=True,
@ -944,7 +944,7 @@ class FakeWindowsTest(BaseTestCase):
r = http( r = http(
'--output', '--output',
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'), os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
'--pretty', '--pretty=all',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=TestEnvironment(is_windows=True) env=TestEnvironment(is_windows=True)
@ -962,7 +962,7 @@ class StreamTest(BaseTestCase):
with open(BIN_FILE_PATH, 'rb') as f: with open(BIN_FILE_PATH, 'rb') as f:
r = http( r = http(
'--verbose', '--verbose',
'--pretty', '--pretty=all',
'--stream', '--stream',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
@ -981,7 +981,7 @@ class StreamTest(BaseTestCase):
"""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: with open(BIN_FILE_PATH, 'rb') as f:
r = http( r = http(
'--ugly', '--pretty=none',
'--stream', '--stream',
'--verbose', '--verbose',
'GET', 'GET',
@ -999,7 +999,7 @@ class StreamTest(BaseTestCase):
"""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: with open(BIN_FILE_PATH, 'rb') as f:
r = http( r = http(
'--ugly', '--pretty=none',
'--stream', '--stream',
'--verbose', '--verbose',
'GET', 'GET',
@ -1043,7 +1043,7 @@ class LineEndingsTest(BaseTestCase):
def test_CRLF_ugly_response(self): def test_CRLF_ugly_response(self):
r = http( r = http(
'--ugly', '--pretty=none',
'GET', 'GET',
httpbin('/get') httpbin('/get')
) )
@ -1051,7 +1051,7 @@ class LineEndingsTest(BaseTestCase):
def test_CRLF_formatted_response(self): def test_CRLF_formatted_response(self):
r = http( r = http(
'--format', '--pretty=format',
'GET', 'GET',
httpbin('/get') httpbin('/get')
) )
@ -1060,7 +1060,7 @@ class LineEndingsTest(BaseTestCase):
def test_CRLF_ugly_request(self): def test_CRLF_ugly_request(self):
r = http( r = http(
'--ugly', '--pretty=none',
'--print=HB', '--print=HB',
'GET', 'GET',
httpbin('/get') httpbin('/get')
@ -1069,7 +1069,7 @@ class LineEndingsTest(BaseTestCase):
def test_CRLF_formatted_request(self): def test_CRLF_formatted_request(self):
r = http( r = http(
'--format', '--pretty=format',
'--print=HB', '--print=HB',
'GET', 'GET',
httpbin('/get') httpbin('/get')