JSON session data, `httpie' management command.

This commit is contained in:
Jakub Roztocil 2012-08-18 23:03:31 +02:00
parent ff9f23da5b
commit 8a9cedb16e
8 changed files with 253 additions and 79 deletions

View File

@ -506,31 +506,30 @@ path. The path can also be configured via the environment variable
Sessions
========
*This is an experimental feature.*
HTTPie supports named sessions, where several options and cookies sent
by the server persist between requests:
.. code-block:: bash
http --session=user1 --auth=user1:password example.org
http --session=user1 --auth=user1:password example.org X-Foo:Bar
Now you can always refer to the session by passing ``--session=user1``,
and the credentials and cookies will be reused:
and the credentials, custom headers and cookies will be reused:
.. code-block:: bash
http --session=user1 GET example.org
Since sessions are named, you can switch between multiple sessions:
.. code-block:: bash
http --session=user2 --auth=user2:password example.org
http --session=user1 example.org
Note that session cookies respect the cookie domain and path.
Session data are stored in ``~/.httpie/sessions/<name>.pickle``.
Session data are stored in ``~/.httpie/sessions/<name>.json``.
You can view and manipulate existing sessions via the ``httpie`` management
command, see ``httpie --help``.
==============

View File

@ -224,11 +224,10 @@ misc.add_argument(
'--session', metavar='SESSION_NAME',
help=_('''
Create or reuse a session.
Withing a session, values of --auth, --timeout,
--verify, --proxies, headers, as well as any
cookies sent by the server are persistent between requests.
Withing a session, custom headers, auth credential, as well as any
cookies sent by the server persist between requests.
You can use the `httpie' management command to manipulate
and inspect existing sessions. See `httpie session'.
and inspect existing sessions. See `httpie --help'.
''')
)

View File

@ -4,12 +4,15 @@ from pprint import pformat
import requests
import requests.auth
from requests.defaults import defaults
from .import sessions
from . import sessions
from . import __version__
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
DEFAULT_UA = 'HTTPie/%s' % __version__
def get_response(args):
@ -29,25 +32,24 @@ def get_response(args):
def get_requests_kwargs(args):
"""Send the request and return a `request.Response`."""
base_headers = defaults['base_headers'].copy()
base_headers['User-Agent'] = DEFAULT_UA
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'
base_headers['Accept'] = 'application/json'
if args.data:
base_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
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = FORM
base_headers['Content-Type'] = FORM
credentials = None
if args.auth:
@ -71,7 +73,10 @@ def get_requests_kwargs(args):
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.allow_redirects,
'params': args.params
'params': args.params,
'config': {
'base_headers': base_headers
}
}
return kwargs

View File

@ -18,8 +18,6 @@ except ImportError:
from requests.structures import CaseInsensitiveDict
from requests.compat import str, urlparse
from . import __version__
HTTP_POST = 'POST'
HTTP_GET = 'GET'
@ -79,7 +77,6 @@ PRETTY_STDOUT_TTY_ONLY = object()
# Defaults
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
DEFAULT_UA = 'HTTPie/%s' % __version__
class Parser(argparse.ArgumentParser):
@ -193,7 +190,6 @@ class Parser(argparse.ArgumentParser):
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = ParamDict() if args.form else OrderedDict()
args.files = OrderedDict()
args.params = ParamDict()

30
httpie/manage.py Normal file
View File

@ -0,0 +1,30 @@
"""
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_actions(subparsers)
def main():
args = parser.parse_args()
args.action(args)
if __name__ == '__main__':
main()

View File

@ -135,7 +135,7 @@ def make_stream(env, args):
elif args.prettify:
Stream = partial(
PrettyStream if args.stream else BufferedPrettyStream,
processor=OutputProcessor(env, groups=args.prettify,
processor=OutputProcessor(env=env, groups=args.prettify,
pygments_style=args.style),
env=env)
else:
@ -343,7 +343,7 @@ class BaseProcessor(object):
enabled = True
def __init__(self, env, **kwargs):
def __init__(self, env=Environment(), **kwargs):
"""
:param env: an class:`Environment` instance
:param kwargs: additional keyword argument that some
@ -406,7 +406,8 @@ class PygmentsProcessor(BaseProcessor):
return
try:
style = get_style_by_name(self.kwargs['pygments_style'])
style = get_style_by_name(
self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound:
style = Solarized256Style
@ -460,7 +461,7 @@ class OutputProcessor(object):
]
}
def __init__(self, env, groups, **kwargs):
def __init__(self, groups, env=Environment(), **kwargs):
"""
:param env: a :class:`models.Environment` instance
:param groups: the groups of processors to be applied

View File

@ -1,68 +1,211 @@
import os
import pickle
import errno
from requests import Session
"""Persistent, JSON-serialized sessions.
"""
import os
import sys
import json
import glob
import errno
import codecs
import subprocess
from requests import Session as RSession
from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from . import __version__
from .config import CONFIG_DIR
from .output import PygmentsProcessor
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)
session = Session.load(name)
# Update session headers with the request headers.
session['headers'].update(request_kwargs.get('headers', {}))
auth = request_kwargs.get('auth', None)
if auth:
session.auth = auth
elif session.auth:
request_kwargs['auth'] = session.auth
# Use the merged headers for the request
request_kwargs['headers'] = session['headers']
rsession = RSession(cookies=session.cookies)
try:
response = session.request(**request_kwargs)
response = rsession.request(**request_kwargs)
except Exception:
raise
else:
save(session, name)
session.cookies = rsession.cookies
session.save()
return response
def split_kwargs(requests_kwargs):
session = {}
request = {}
session_attrs = [
'auth', 'timeout',
'verify', 'proxies',
'params'
]
class Session(dict):
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 __init__(self, name, *args, **kwargs):
super(Session, self).__init__(*args, **kwargs)
self.name = name
self.setdefault('cookies', {})
self.setdefault('headers', {})
@property
def path(self):
return type(self).get_path(self.name)
@property
def cookies(self):
jar = RequestsCookieJar()
for name, cookie_dict in self['cookies'].items():
cookie = create_cookie(
name, cookie_dict.pop('value'), **cookie_dict)
jar.set_cookie(cookie)
jar.clear_expired_cookies()
return jar
@cookies.setter
def cookies(self, jar):
exclude = [
'_rest', 'name', 'port_specified',
'domain_specified', 'domain_initial_dot',
'path_specified'
]
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 exclude:
cookie_dict[k] = v
self['cookies'][name] = cookie_dict
@property
def auth(self):
auth = self.get('auth', None)
if not auth:
return None
Auth = {'basic': HTTPBasicAuth,
'digest': HTTPDigestAuth}[auth['type']]
return Auth(auth['username'], auth['password'])
def get_path(name):
try:
os.makedirs(SESSIONS_DIR, mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
@auth.setter
def auth(self, cred):
self['auth'] = {
'type': {HTTPBasicAuth: 'basic',
HTTPDigestAuth: 'digest'}[type(cred)],
'username': cred.username,
'password': cred.password,
}
return os.path.join(SESSIONS_DIR, name + '.pickle')
def save(self):
self['__version__'] = __version__
with open(self.path, 'wb') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write(b'\n')
@classmethod
def load(cls, name):
try:
with open(cls.get_path(name), 'rt') as f:
try:
data = json.load(f)
except ValueError as e:
raise ValueError('Invalid session: %s [%s]' %
(e.message, f.name))
return cls(name, data)
except IOError as e:
if e.errno != errno.ENOENT:
raise
return cls(name)
@classmethod
def get_path(cls, 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 + '.json')
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 show_action(args):
if not args.name:
for fn in sorted(glob.glob1(SESSIONS_DIR, '*.json')):
print(os.path.splitext(fn)[0])
return
path = Session.get_path(args.name)
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')
print(PygmentsProcessor().process_body(
f.read(), 'application/json', 'json'))
print('')
def save(session, name):
with open(get_path(name), 'wb') as f:
pickle.dump(session, f)
def delete_action(args):
if not args.name:
for path in glob.glob(os.path.join(SESSIONS_DIR, '*.json')):
os.unlink(path)
return
path = Session.get_path(args.name)
if not os.path.exists(path):
sys.stderr.write('Session "%s" does not exist [%s].\n'
% (args.name, path))
sys.exit(1)
else:
os.unlink(path)
def edit_action(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.get_path(args.name))
subprocess.call(command)
def add_actions(subparsers):
# Show
show = subparsers.add_parser('session-show', help='list or show sessions')
show.set_defaults(action=show_action)
show.add_argument('name', nargs='?',
help='When omitted, HTTPie prints a list of existing sessions.'
' When specified, the session data is printed.')
# Edit
edit = subparsers.add_parser('session-edit', help='edit a session in $EDITOR')
edit.set_defaults(action=edit_action)
edit.add_argument('name')
# Delete
delete = subparsers.add_parser('session-delete', help='delete a session')
delete.set_defaults(action=delete_action)
delete_group = delete.add_mutually_exclusive_group(required=True)
delete_group.add_argument(
'--all', action='store_true',
help='Delete all sessions from %s' % SESSIONS_DIR)
delete_group.add_argument(
'name', nargs='?',
help='The name of the session to be deleted. ' \
'To see a list existing sessions, run `httpie sessions show\'.')

View File

@ -47,6 +47,7 @@ setup(
entry_points={
'console_scripts': [
'http = httpie.__main__:main',
'httpie = httpie.manage:main',
],
},
install_requires=requirements,