From 0b3bad9c8174ffd2ab5d2da9ede7857f6dfeff95 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Fri, 17 Aug 2012 23:23:02 +0200 Subject: [PATCH] Added initial support for persistent sessions. --- README.rst | 25 +++++++++++++++ httpie/cli.py | 10 ++++++ httpie/client.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ httpie/config.py | 6 ++++ httpie/core.py | 67 ++-------------------------------------- httpie/sessions.py | 68 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 65 deletions(-) create mode 100644 httpie/client.py create mode 100644 httpie/config.py create mode 100644 httpie/sessions.py diff --git a/README.rst b/README.rst index 2911b2b4..153229e0 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 ============== @@ -958,6 +982,7 @@ Changelog *You can click a version name to see a diff with the previous one.* * `0.2.8-alpha`_ + * Added session support. * CRLF HTTP header field separation in the output. * Added exit status code ``2`` for timed-out requests. * Added the option to separate colorizing and formatting diff --git a/httpie/cli.py b/httpie/cli.py index 6ca3bd77..638120e2 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -169,6 +169,16 @@ 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]', 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 71b730e8..1fac2669 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -10,79 +10,22 @@ Invocation flow: 5. Exit. """ -from _socket import gaierror import sys -import json import errno -from pprint import pformat import requests -import requests.auth from requests.compat import str 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 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 +64,7 @@ def main(args=sys.argv[1:], env=Environment()): try: args = parser.parse_args(args=args, env=env) - kwargs = get_requests_kwargs(args) - - if args.debug: - sys.stderr.write( - '\n>>> requests.request(%s)\n\n' % pformat(kwargs)) - - response = requests.request(**kwargs) + response = get_response(args) if args.check_status: status = get_exist_status(response.status_code, 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)