forked from extern/httpie-cli
Added anonymous sessions (--session=/file/path.json).
This commit is contained in:
parent
76eebeac2a
commit
87c59ae561
32
README.rst
32
README.rst
@ -937,13 +937,17 @@ Streamed output by small chunks alá ``tail -f``:
|
|||||||
Sessions
|
Sessions
|
||||||
========
|
========
|
||||||
|
|
||||||
By default, every request is completely independent of the previous ones.
|
By default, every request is completely independent of any previous ones.
|
||||||
HTTPie also supports persistent sessions, where custom headers (except for the
|
HTTPie also supports persistent sessions, where custom headers (except for the
|
||||||
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
||||||
(manually specified or sent by the server) persist between requests
|
(manually specified or sent by the server) persist between requests
|
||||||
to the same host.
|
to the same host.
|
||||||
|
|
||||||
Create a new session named ``user1``:
|
--------------
|
||||||
|
Named Sessions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Create a new session named ``user1`` for ``example.org``:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@ -966,14 +970,30 @@ To use a session without updating it from the request/response exchange
|
|||||||
once it is created, specify the session name via
|
once it is created, specify the session name via
|
||||||
``--session-read-only=SESSION_NAME`` instead.
|
``--session-read-only=SESSION_NAME`` instead.
|
||||||
|
|
||||||
Session data are stored in JSON files in the directory
|
Named sessions' data is stored in JSON files in the directory
|
||||||
``~/.httpie/sessions/<host>/<name>.json``
|
``~/.httpie/sessions/<host>/<name>.json``
|
||||||
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||||
|
|
||||||
|
------------------
|
||||||
|
Anonymous Sessions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Instead of a name, you can also directly specify a path to a session file. This
|
||||||
|
allows for re-using session across multiple hosts:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ http --session=/tmp/session.json example.org
|
||||||
|
$ http --session=/tmp/session.json admin.example.org
|
||||||
|
$ http --session=~/.httpie/sessions/another.example.org/test.json example.org
|
||||||
|
$ http --session-read-only=/tmp/session.json example.org
|
||||||
|
|
||||||
|
|
||||||
**Warning:** All session data, including credentials, cookie data,
|
**Warning:** All session data, including credentials, cookie data,
|
||||||
and custom headers are stored in plain text.
|
and custom headers are stored in plain text.
|
||||||
|
|
||||||
Session files can also be created and edited manually in a text editor.
|
Note that session files can also be created and edited manually in a text
|
||||||
|
editor; they are plain JSON.
|
||||||
|
|
||||||
See also `Config`_.
|
See also `Config`_.
|
||||||
|
|
||||||
@ -1164,6 +1184,8 @@ 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.6.0-dev`_
|
* `0.6.0-dev`_
|
||||||
|
* ``--session`` and ``--session-read-only`` now also accept paths to
|
||||||
|
session files (eg. ``http --session=/tmp/session.json example.org``).
|
||||||
* `0.5.1`_ (2013-05-13)
|
* `0.5.1`_ (2013-05-13)
|
||||||
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
||||||
anymore as they are request-specific.
|
anymore as they are request-specific.
|
||||||
|
@ -7,13 +7,13 @@ from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
|||||||
|
|
||||||
from . import __doc__
|
from . import __doc__
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
from .sessions import DEFAULT_SESSIONS_DIR
|
||||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
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_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||||
|
|
||||||
|
|
||||||
def _(text):
|
def _(text):
|
||||||
@ -256,19 +256,21 @@ output_options.add_argument(
|
|||||||
''')
|
''')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Sessions
|
# Sessions
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
sessions = parser.add_argument_group(title='Sessions')\
|
sessions = parser.add_argument_group(title='Sessions')\
|
||||||
.add_mutually_exclusive_group(required=False)
|
.add_mutually_exclusive_group(required=False)
|
||||||
|
|
||||||
|
session_name_validator = SessionNameValidator(
|
||||||
|
'Session name contains invalid characters.')
|
||||||
|
|
||||||
sessions.add_argument(
|
sessions.add_argument(
|
||||||
'--session',
|
'--session',
|
||||||
metavar='SESSION_NAME',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
type=RegexValidator(
|
type=session_name_validator,
|
||||||
Session.VALID_NAME_PATTERN,
|
|
||||||
'Session name contains invalid characters.'
|
|
||||||
),
|
|
||||||
help=_('''
|
help=_('''
|
||||||
Create, or reuse and update a session.
|
Create, or reuse and update a session.
|
||||||
Within a session, custom headers, auth credential, as well as any
|
Within a session, custom headers, auth credential, as well as any
|
||||||
@ -278,7 +280,8 @@ sessions.add_argument(
|
|||||||
)
|
)
|
||||||
sessions.add_argument(
|
sessions.add_argument(
|
||||||
'--session-read-only',
|
'--session-read-only',
|
||||||
metavar='SESSION_NAME',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
|
type=session_name_validator,
|
||||||
help=_('''
|
help=_('''
|
||||||
Create or read a session without updating it form the
|
Create or read a session without updating it form the
|
||||||
request/response exchange.
|
request/response exchange.
|
||||||
@ -289,6 +292,7 @@ sessions.add_argument(
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Authentication
|
# Authentication
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# ``requests.request`` keyword arguments.
|
# ``requests.request`` keyword arguments.
|
||||||
auth = parser.add_argument_group(title='Authentication')
|
auth = parser.add_argument_group(title='Authentication')
|
||||||
auth.add_argument(
|
auth.add_argument(
|
||||||
@ -312,8 +316,9 @@ auth.add_argument(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
# Network
|
# Network
|
||||||
#############################################
|
###############################################################################
|
||||||
|
|
||||||
network = parser.add_argument_group(title='Network')
|
network = parser.add_argument_group(title='Network')
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ def get_response(args, config_dir):
|
|||||||
else:
|
else:
|
||||||
response = sessions.get_response(
|
response = sessions.get_response(
|
||||||
config_dir=config_dir,
|
config_dir=config_dir,
|
||||||
name=args.session or args.session_read_only,
|
session_name=args.session or args.session_read_only,
|
||||||
requests_kwargs=requests_kwargs,
|
requests_kwargs=requests_kwargs,
|
||||||
read_only=bool(args.session_read_only),
|
read_only=bool(args.session_read_only),
|
||||||
)
|
)
|
||||||
|
@ -16,9 +16,8 @@ DEFAULT_CONFIG_DIR = os.environ.get(
|
|||||||
class BaseConfigDict(dict):
|
class BaseConfigDict(dict):
|
||||||
|
|
||||||
name = None
|
name = None
|
||||||
help = None
|
helpurl = None
|
||||||
about = None
|
about = None
|
||||||
|
|
||||||
directory = DEFAULT_CONFIG_DIR
|
directory = DEFAULT_CONFIG_DIR
|
||||||
|
|
||||||
def __init__(self, directory=None, *args, **kwargs):
|
def __init__(self, directory=None, *args, **kwargs):
|
||||||
@ -29,18 +28,24 @@ class BaseConfigDict(dict):
|
|||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
return self[item]
|
return self[item]
|
||||||
|
|
||||||
@property
|
def _get_path(self):
|
||||||
def path(self):
|
"""Return the config file path without side-effects."""
|
||||||
try:
|
|
||||||
os.makedirs(self.directory, mode=0o700)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
return os.path.join(self.directory, self.name + '.json')
|
return os.path.join(self.directory, self.name + '.json')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
"""Return the config file path creating basedir, if needed."""
|
||||||
|
path = self._get_path()
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(path), mode=0o700)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
return path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_new(self):
|
def is_new(self):
|
||||||
return not os.path.exists(self.path)
|
return not os.path.exists(self._get_path())
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
try:
|
try:
|
||||||
@ -61,8 +66,8 @@ class BaseConfigDict(dict):
|
|||||||
self['__meta__'] = {
|
self['__meta__'] = {
|
||||||
'httpie': __version__
|
'httpie': __version__
|
||||||
}
|
}
|
||||||
if self.help:
|
if self.helpurl:
|
||||||
self['__meta__']['help'] = self.help
|
self['__meta__']['help'] = self.helpurl
|
||||||
|
|
||||||
if self.about:
|
if self.about:
|
||||||
self['__meta__']['about'] = self.about
|
self['__meta__']['about'] = self.about
|
||||||
@ -82,7 +87,7 @@ class BaseConfigDict(dict):
|
|||||||
class Config(BaseConfigDict):
|
class Config(BaseConfigDict):
|
||||||
|
|
||||||
name = 'config'
|
name = 'config'
|
||||||
help = 'https://github.com/jkbr/httpie#config'
|
helpurl = 'https://github.com/jkbr/httpie#config'
|
||||||
about = 'HTTPie configuration file'
|
about = 'HTTPie configuration file'
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
|
@ -20,6 +20,7 @@ except ImportError:
|
|||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from .compat import urlsplit, str
|
from .compat import urlsplit, str
|
||||||
|
from .sessions import VALID_SESSION_NAME_PATTERN
|
||||||
|
|
||||||
|
|
||||||
HTTP_POST = 'POST'
|
HTTP_POST = 'POST'
|
||||||
@ -373,24 +374,15 @@ class KeyValue(object):
|
|||||||
return self.__dict__ == other.__dict__
|
return self.__dict__ == other.__dict__
|
||||||
|
|
||||||
|
|
||||||
def session_name_arg_type(name):
|
class SessionNameValidator(object):
|
||||||
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 __init__(self, error_message):
|
||||||
class RegexValidator(object):
|
|
||||||
|
|
||||||
def __init__(self, pattern, error_message):
|
|
||||||
self.pattern = re.compile(pattern)
|
|
||||||
self.error_message = error_message
|
self.error_message = error_message
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
if not self.pattern.search(value):
|
# Session name can be a path or just a name.
|
||||||
|
if (os.path.sep not in value
|
||||||
|
and not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||||
raise ArgumentError(None, self.error_message)
|
raise ArgumentError(None, self.error_message)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import glob
|
|
||||||
import errno
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.cookies import RequestsCookieJar, create_cookie
|
from requests.cookies import RequestsCookieJar, create_cookie
|
||||||
@ -17,26 +14,36 @@ from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
|||||||
|
|
||||||
SESSIONS_DIR_NAME = 'sessions'
|
SESSIONS_DIR_NAME = 'sessions'
|
||||||
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
|
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
|
||||||
|
VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
||||||
|
|
||||||
# Request headers starting with these prefixes won't be stored in sessions.
|
# Request headers starting with these prefixes won't be stored in sessions.
|
||||||
# They are specific to each request.
|
# They are specific to each request.
|
||||||
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
||||||
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
|
SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-']
|
||||||
|
|
||||||
|
|
||||||
def get_response(name, requests_kwargs, config_dir, read_only=False):
|
def get_response(session_name, requests_kwargs, config_dir, read_only=False):
|
||||||
"""Like `client.get_response`, but applies permanent
|
"""Like `client.get_response`, but applies permanent
|
||||||
aspects of the session to the request.
|
aspects of the session to the request.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
|
if os.path.sep in session_name:
|
||||||
host = Host(
|
path = os.path.expanduser(session_name)
|
||||||
root_dir=sessions_dir,
|
else:
|
||||||
name=requests_kwargs['headers'].get('Host', None)
|
hostname = (
|
||||||
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
requests_kwargs['headers'].get('Host', None)
|
||||||
)
|
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
||||||
session = Session(host, name)
|
)
|
||||||
|
|
||||||
|
assert re.match('^[a-zA-Z0-9_.:-]+$', hostname)
|
||||||
|
|
||||||
|
# host:port => host_port
|
||||||
|
hostname = hostname.replace(':', '_')
|
||||||
|
path = os.path.join(config_dir,
|
||||||
|
SESSIONS_DIR_NAME,
|
||||||
|
hostname,
|
||||||
|
session_name + '.json')
|
||||||
|
|
||||||
|
session = Session(path)
|
||||||
session.load()
|
session.load()
|
||||||
|
|
||||||
# Merge request and session headers to get final headers for this request.
|
# Merge request and session headers to get final headers for this request.
|
||||||
@ -68,69 +75,13 @@ def get_response(name, requests_kwargs, config_dir, read_only=False):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class Host(object):
|
|
||||||
"""A host is a per-host directory on the disk containing sessions files."""
|
|
||||||
|
|
||||||
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 an iterator yielding `Session` instances."""
|
|
||||||
for fn in sorted(glob.glob1(self.path, '*.json')):
|
|
||||||
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):
|
|
||||||
path = os.path.join(self.root_dir, self._quote_name(self.name))
|
|
||||||
try:
|
|
||||||
os.makedirs(path, mode=0o700)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
class Session(BaseConfigDict):
|
class Session(BaseConfigDict):
|
||||||
|
helpurl = 'https://github.com/jkbr/httpie#sessions'
|
||||||
help = 'https://github.com/jkbr/httpie#sessions'
|
|
||||||
about = 'HTTPie session file'
|
about = 'HTTPie session file'
|
||||||
|
|
||||||
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
def __init__(self, path, *args, **kwargs):
|
||||||
|
|
||||||
def __init__(self, host, name, *args, **kwargs):
|
|
||||||
assert self.VALID_NAME_PATTERN.match(name)
|
|
||||||
super(Session, self).__init__(*args, **kwargs)
|
super(Session, self).__init__(*args, **kwargs)
|
||||||
self.host = host
|
self._path = path
|
||||||
self.name = name
|
|
||||||
self['headers'] = {}
|
self['headers'] = {}
|
||||||
self['cookies'] = {}
|
self['cookies'] = {}
|
||||||
self['auth'] = {
|
self['auth'] = {
|
||||||
@ -139,13 +90,8 @@ class Session(BaseConfigDict):
|
|||||||
'password': None
|
'password': None
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
def _get_path(self):
|
||||||
def directory(self):
|
return self._path
|
||||||
return self.host.path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def verbose_name(self):
|
|
||||||
return '%s %s %s' % (self.host.name, self.name, self.path)
|
|
||||||
|
|
||||||
def update_headers(self, request_headers):
|
def update_headers(self, request_headers):
|
||||||
"""
|
"""
|
||||||
|
@ -1464,6 +1464,28 @@ class SessionTest(BaseTestCase):
|
|||||||
# Should be the same as before r2.
|
# Should be the same as before r2.
|
||||||
self.assertDictEqual(r1.json, r3.json)
|
self.assertDictEqual(r1.json, r3.json)
|
||||||
|
|
||||||
|
def test_session_by_path(self):
|
||||||
|
session_path = os.path.join(self.config_dir, 'session-by-path.json')
|
||||||
|
|
||||||
|
r1 = http(
|
||||||
|
'--session=' + session_path,
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
'Foo:Bar',
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r1)
|
||||||
|
|
||||||
|
r2 = http(
|
||||||
|
'--session=' + session_path,
|
||||||
|
'GET',
|
||||||
|
httpbin('/get'),
|
||||||
|
env=self.env
|
||||||
|
)
|
||||||
|
self.assertIn(OK, r2)
|
||||||
|
|
||||||
|
self.assertEqual(r2.json['headers']['Foo'], 'Bar')
|
||||||
|
|
||||||
|
|
||||||
class DownloadUtilsTest(BaseTestCase):
|
class DownloadUtilsTest(BaseTestCase):
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user