diff --git a/README.rst b/README.rst index a3d7b450..f72a63db 100644 --- a/README.rst +++ b/README.rst @@ -502,6 +502,30 @@ path. The path can also be configured via the environment variable ``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/.pickle``. + ============== Output Options ============== @@ -700,12 +724,14 @@ Also, the following formatting is applied: One of these options can be used to control output processing: -=============== ============================================================== -``--pretty`` Apply both colors and formatting. Default for terminal output. -``--colors`` Apply colors. -``--format`` Apply formatting. -``--ugly, -u`` Disables output processing. Default for redirected output. -=============== ============================================================== +==================== ======================================================== +``--pretty=all`` Apply both colors and formatting. + Default for terminal output. +``--pretty=colors`` Apply colors. +``--pretty=format`` Apply formatting. +``--pretty=none`` Disables output processing. + Default for redirected output. +==================== ======================================================== ----------- Binary data @@ -743,8 +769,7 @@ Redirected Output HTTPie uses **different defaults** for redirected output than for `terminal output`_: -* Formatting and colors aren't applied (unless ``--pretty``, ``--format``, - or ``--colors`` is set). +* Formatting and colors aren't applied (unless ``--pretty`` is specified). * Only the response body is printed (unless one of the `output options`_ is set). * Also, binary data isn't suppressed. @@ -771,7 +796,7 @@ Force colorizing and formatting, and show both the request and the response in .. 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 @@ -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`` 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 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.* -* `0.2.8dev`_ - * Fixed colorized output on Windows with Python 3. +* `0.2.8-alpha`_ + * Added persistent session support. * Fixed installation on Windows with Python 3. + * Fixed colorized output on Windows with Python 3. * CRLF HTTP header field separation in the output. * Added exit status code ``2`` for timed-out requests. - * Added ``--colors`` and ``--format`` in addition to ``--pretty``, to - be able to separate colorizing and formatting. + * Added the option to separate colorizing and formatting + (``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``). + ``--ugly`` has bee removed in favor of ``--pretty=none``. * `0.2.7`_ (2012-08-07) * Compatibility with Requests 0.13.6. * Streamed terminal output. ``--stream`` / ``-S`` can be used to enable streaming also with ``--pretty`` and to ensure a more frequent output flushing. * Support for efficient large file downloads. - * Sort headers by name (unless ``--ugly``). + * Sort headers by name (unless ``--pretty=none``). * Response body is fetched only when needed (e.g., not with ``--headers``). * Improved content type matching. * Updated Solarized color scheme. @@ -1042,7 +1069,7 @@ Changelog .. _0.2.5: https://github.com/jkbr/httpie/compare/0.2.2...0.2.5 .. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6 .. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7 -.. _0.2.8dev: https://github.com/jkbr/httpie/compare/0.2.7...master +.. _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 .. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst .. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE diff --git a/httpie/__init__.py b/httpie/__init__.py index 02688502..42cc96fa 100644 --- a/httpie/__init__.py +++ b/httpie/__init__.py @@ -3,7 +3,7 @@ HTTPie - cURL for humans. """ __author__ = 'Jakub Roztocil' -__version__ = '0.2.8dev' +__version__ = '0.2.8-alpha' __licence__ = 'BSD' diff --git a/httpie/cli.py b/httpie/cli.py index 3f84babe..638120e2 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -14,8 +14,7 @@ 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_STDOUT_TTY_ONLY, PRETTY_ALL, - PRETTY_FORMAT, PRETTY_COLORS) + PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY) def _(text): @@ -69,29 +68,17 @@ parser.add_argument( ) ) -prettify = parser.add_mutually_exclusive_group(required=False) -prettify.add_argument( - '--pretty', dest='prettify', action='store_const', const=PRETTY_ALL, - default=PRETTY_STDOUT_TTY_ONLY, + +parser.add_argument( + '--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY, + choices=sorted(PRETTY_MAP.keys()), 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 + (default for terminal output), "colors", or "format". ''') ) -prettify.add_argument( - '--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. - ''') -) output_options = parser.add_mutually_exclusive_group(required=False) output_options.add_argument('--print', '-p', dest='output_options', @@ -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. parser.add_argument( - '--auth', '-a', metavar='USER:PASS', + '--auth', '-a', metavar='USER[:PASS]', type=AuthCredentialsArgType(SEP_CREDENTIALS), help=_(''' username:password. diff --git a/httpie/client.py b/httpie/client.py new file mode 100644 index 00000000..e36931ea --- /dev/null +++ b/httpie/client.py @@ -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 diff --git a/httpie/config.py b/httpie/config.py new file mode 100644 index 00000000..c286611e --- /dev/null +++ b/httpie/config.py @@ -0,0 +1,6 @@ +import os + +__author__ = 'jakub' + + +CONFIG_DIR = os.path.expanduser('~/.httpie') diff --git a/httpie/core.py b/httpie/core.py index 4499bd74..e658ad5d 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -11,77 +11,21 @@ Invocation flow: """ import sys -import json import errno -from pprint import pformat import requests -import requests.auth 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 .client import get_response from .models import Environment from .output import output_stream, write, write_with_colors_win_p3k 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): """Translate HTTP status code to exit status.""" @@ -121,13 +65,7 @@ def main(args=sys.argv[1:], env=Environment()): try: args = parser.parse_args(args=args, env=env) - requests_kwargs = get_requests_kwargs(args) - - if args.debug: - sys.stderr.write( - '\n>>> requests.request(%s)\n\n' % pformat(requests_kwargs)) - - response = requests.request(**requests_kwargs) + response = get_response(args) if args.check_status: status = get_exist_status(response.status_code, diff --git a/httpie/input.py b/httpie/input.py index a4a807f2..a4df6125 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -67,9 +67,12 @@ OUTPUT_OPTIONS = frozenset([ ]) # Pretty -PRETTY_ALL = ['format', 'colors'] -PRETTY_FORMAT = ['format'] -PRETTY_COLORS = ['colors'] +PRETTY_MAP = { + 'all': ['format', 'colors'], + 'colors': ['colors'], + 'format': ['format'], + 'none': [] +} PRETTY_STDOUT_TTY_ONLY = object() @@ -114,6 +117,7 @@ class Parser(argparse.ArgumentParser): env.stdout_isatty = False self._process_output_options(args, env) + self._process_pretty_options(args, env) self._guess_method(args, env) self._parse_items(args) @@ -128,10 +132,6 @@ class Parser(argparse.ArgumentParser): # Stdin already read (if not a tty) so it's save to prompt. 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 @@ -246,6 +246,14 @@ class Parser(argparse.ArgumentParser): if unknown: self.error('Unknown output options: %s' % ','.join(unknown)) + def _process_pretty_options(self, args, env): + if args.prettify == PRETTY_STDOUT_TTY_ONLY: + args.prettify = PRETTY_MAP['all' if env.stdout_isatty else 'none'] + elif args.prettify and env.is_windows: + self.error('Only terminal output can be prettified on Windows.') + else: + args.prettify = PRETTY_MAP[args.prettify] + class ParseError(Exception): pass diff --git a/httpie/sessions.py b/httpie/sessions.py new file mode 100644 index 00000000..a1997f45 --- /dev/null +++ b/httpie/sessions.py @@ -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) diff --git a/tests/tests.py b/tests/tests.py index 76ad3108..eda3b446 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -524,7 +524,7 @@ class ImplicitHTTPMethodTest(BaseTestCase): class PrettyOptionsTest(BaseTestCase): - """Test the --pretty / --ugly flag handling.""" + """Test the --pretty flag handling.""" def test_pretty_enabled_by_default(self): r = http( @@ -543,7 +543,7 @@ class PrettyOptionsTest(BaseTestCase): def test_force_pretty(self): r = http( - '--pretty', + '--pretty=all', 'GET', httpbin('/get'), env=TestEnvironment(stdout_isatty=False, colors=256), @@ -552,7 +552,7 @@ class PrettyOptionsTest(BaseTestCase): def test_force_ugly(self): r = http( - '--ugly', + '--pretty=none', 'GET', httpbin('/get'), ) @@ -565,7 +565,7 @@ class PrettyOptionsTest(BaseTestCase): """ r = http( '--print=B', - '--pretty', + '--pretty=all', httpbin('/post'), 'Content-Type:text/foo+json', 'a=b', @@ -576,7 +576,7 @@ class PrettyOptionsTest(BaseTestCase): def test_colors_option(self): r = http( '--print=B', - '--colors', + '--pretty=colors', 'GET', httpbin('/get'), 'a=b', @@ -590,7 +590,7 @@ class PrettyOptionsTest(BaseTestCase): def test_format_option(self): r = http( '--print=B', - '--format', + '--pretty=format', 'GET', httpbin('/get'), 'a=b', @@ -737,7 +737,7 @@ class BinaryResponseDataTest(BaseTestCase): def test_binary_suppresses_when_not_terminal_but_pretty(self): r = http( - '--pretty', + '--pretty=all', 'GET', self.url, env=TestEnvironment(stdin_isatty=True, @@ -944,7 +944,7 @@ class FakeWindowsTest(BaseTestCase): r = http( '--output', os.path.join(tempfile.gettempdir(), '__httpie_test_output__'), - '--pretty', + '--pretty=all', 'GET', httpbin('/get'), env=TestEnvironment(is_windows=True) @@ -962,7 +962,7 @@ class StreamTest(BaseTestCase): with open(BIN_FILE_PATH, 'rb') as f: r = http( '--verbose', - '--pretty', + '--pretty=all', '--stream', 'GET', httpbin('/get'), @@ -981,7 +981,7 @@ class StreamTest(BaseTestCase): """Test that --stream works with non-prettified redirected terminal output.""" with open(BIN_FILE_PATH, 'rb') as f: r = http( - '--ugly', + '--pretty=none', '--stream', '--verbose', 'GET', @@ -999,7 +999,7 @@ class StreamTest(BaseTestCase): """Test that --stream works with non-prettified redirected terminal output.""" with open(BIN_FILE_PATH, 'rb') as f: r = http( - '--ugly', + '--pretty=none', '--stream', '--verbose', 'GET', @@ -1043,7 +1043,7 @@ class LineEndingsTest(BaseTestCase): def test_CRLF_ugly_response(self): r = http( - '--ugly', + '--pretty=none', 'GET', httpbin('/get') ) @@ -1051,7 +1051,7 @@ class LineEndingsTest(BaseTestCase): def test_CRLF_formatted_response(self): r = http( - '--format', + '--pretty=format', 'GET', httpbin('/get') ) @@ -1060,7 +1060,7 @@ class LineEndingsTest(BaseTestCase): def test_CRLF_ugly_request(self): r = http( - '--ugly', + '--pretty=none', '--print=HB', 'GET', httpbin('/get') @@ -1069,7 +1069,7 @@ class LineEndingsTest(BaseTestCase): def test_CRLF_formatted_request(self): r = http( - '--format', + '--pretty=format', '--print=HB', 'GET', httpbin('/get')