mirror of
https://github.com/httpie/cli.git
synced 2024-11-21 23:33:12 +01:00
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
|
||||
========
|
||||
|
||||
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
|
||||
ones starting with ``Content-`` or ``If-``), authorization, and cookies
|
||||
(manually specified or sent by the server) persist between requests
|
||||
to the same host.
|
||||
|
||||
Create a new session named ``user1``:
|
||||
--------------
|
||||
Named Sessions
|
||||
--------------
|
||||
|
||||
Create a new session named ``user1`` for ``example.org``:
|
||||
|
||||
.. 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
|
||||
``--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``
|
||||
(``%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,
|
||||
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`_.
|
||||
|
||||
@ -1164,6 +1184,8 @@ Changelog
|
||||
*You can click a version name to see a diff with the previous one.*
|
||||
|
||||
* `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)
|
||||
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
||||
anymore as they are request-specific.
|
||||
|
@ -7,13 +7,13 @@ from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from .sessions import DEFAULT_SESSIONS_DIR, Session
|
||||
from .sessions import DEFAULT_SESSIONS_DIR
|
||||
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||
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_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
|
||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator)
|
||||
|
||||
|
||||
def _(text):
|
||||
@ -256,19 +256,21 @@ output_options.add_argument(
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Sessions
|
||||
###############################################################################
|
||||
|
||||
sessions = parser.add_argument_group(title='Sessions')\
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
|
||||
session_name_validator = SessionNameValidator(
|
||||
'Session name contains invalid characters.')
|
||||
|
||||
sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME',
|
||||
type=RegexValidator(
|
||||
Session.VALID_NAME_PATTERN,
|
||||
'Session name contains invalid characters.'
|
||||
),
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=_('''
|
||||
Create, or reuse and update a session.
|
||||
Within a session, custom headers, auth credential, as well as any
|
||||
@ -278,7 +280,8 @@ sessions.add_argument(
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=_('''
|
||||
Create or read a session without updating it form the
|
||||
request/response exchange.
|
||||
@ -289,6 +292,7 @@ sessions.add_argument(
|
||||
###############################################################################
|
||||
# Authentication
|
||||
###############################################################################
|
||||
|
||||
# ``requests.request`` keyword arguments.
|
||||
auth = parser.add_argument_group(title='Authentication')
|
||||
auth.add_argument(
|
||||
@ -312,8 +316,9 @@ auth.add_argument(
|
||||
)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Network
|
||||
#############################################
|
||||
###############################################################################
|
||||
|
||||
network = parser.add_argument_group(title='Network')
|
||||
|
||||
|
@ -28,7 +28,7 @@ def get_response(args, config_dir):
|
||||
else:
|
||||
response = sessions.get_response(
|
||||
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,
|
||||
read_only=bool(args.session_read_only),
|
||||
)
|
||||
|
@ -16,9 +16,8 @@ DEFAULT_CONFIG_DIR = os.environ.get(
|
||||
class BaseConfigDict(dict):
|
||||
|
||||
name = None
|
||||
help = None
|
||||
helpurl = None
|
||||
about = None
|
||||
|
||||
directory = DEFAULT_CONFIG_DIR
|
||||
|
||||
def __init__(self, directory=None, *args, **kwargs):
|
||||
@ -29,18 +28,24 @@ class BaseConfigDict(dict):
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
try:
|
||||
os.makedirs(self.directory, mode=0o700)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
def _get_path(self):
|
||||
"""Return the config file path without side-effects."""
|
||||
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
|
||||
def is_new(self):
|
||||
return not os.path.exists(self.path)
|
||||
return not os.path.exists(self._get_path())
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
@ -61,8 +66,8 @@ class BaseConfigDict(dict):
|
||||
self['__meta__'] = {
|
||||
'httpie': __version__
|
||||
}
|
||||
if self.help:
|
||||
self['__meta__']['help'] = self.help
|
||||
if self.helpurl:
|
||||
self['__meta__']['help'] = self.helpurl
|
||||
|
||||
if self.about:
|
||||
self['__meta__']['about'] = self.about
|
||||
@ -82,7 +87,7 @@ class BaseConfigDict(dict):
|
||||
class Config(BaseConfigDict):
|
||||
|
||||
name = 'config'
|
||||
help = 'https://github.com/jkbr/httpie#config'
|
||||
helpurl = 'https://github.com/jkbr/httpie#config'
|
||||
about = 'HTTPie configuration file'
|
||||
|
||||
DEFAULTS = {
|
||||
|
@ -20,6 +20,7 @@ except ImportError:
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from .compat import urlsplit, str
|
||||
from .sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
@ -373,24 +374,15 @@ class KeyValue(object):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def session_name_arg_type(name):
|
||||
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
|
||||
class SessionNameValidator(object):
|
||||
|
||||
|
||||
class RegexValidator(object):
|
||||
|
||||
def __init__(self, pattern, error_message):
|
||||
self.pattern = re.compile(pattern)
|
||||
def __init__(self, error_message):
|
||||
self.error_message = error_message
|
||||
|
||||
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)
|
||||
return value
|
||||
|
||||
|
@ -3,9 +3,6 @@
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
import errno
|
||||
import shutil
|
||||
|
||||
import requests
|
||||
from requests.cookies import RequestsCookieJar, create_cookie
|
||||
@ -17,26 +14,36 @@ from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||
|
||||
SESSIONS_DIR_NAME = 'sessions'
|
||||
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.
|
||||
# They are specific to each request.
|
||||
# http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Requests
|
||||
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
|
||||
aspects of the session to the request.
|
||||
|
||||
"""
|
||||
sessions_dir = os.path.join(config_dir, SESSIONS_DIR_NAME)
|
||||
host = Host(
|
||||
root_dir=sessions_dir,
|
||||
name=requests_kwargs['headers'].get('Host', None)
|
||||
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
||||
)
|
||||
session = Session(host, name)
|
||||
if os.path.sep in session_name:
|
||||
path = os.path.expanduser(session_name)
|
||||
else:
|
||||
hostname = (
|
||||
requests_kwargs['headers'].get('Host', None)
|
||||
or urlsplit(requests_kwargs['url']).netloc.split('@')[-1]
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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):
|
||||
|
||||
help = 'https://github.com/jkbr/httpie#sessions'
|
||||
helpurl = 'https://github.com/jkbr/httpie#sessions'
|
||||
about = 'HTTPie session file'
|
||||
|
||||
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
|
||||
|
||||
def __init__(self, host, name, *args, **kwargs):
|
||||
assert self.VALID_NAME_PATTERN.match(name)
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
super(Session, self).__init__(*args, **kwargs)
|
||||
self.host = host
|
||||
self.name = name
|
||||
self._path = path
|
||||
self['headers'] = {}
|
||||
self['cookies'] = {}
|
||||
self['auth'] = {
|
||||
@ -139,13 +90,8 @@ class Session(BaseConfigDict):
|
||||
'password': None
|
||||
}
|
||||
|
||||
@property
|
||||
def directory(self):
|
||||
return self.host.path
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
return '%s %s %s' % (self.host.name, self.name, self.path)
|
||||
def _get_path(self):
|
||||
return self._path
|
||||
|
||||
def update_headers(self, request_headers):
|
||||
"""
|
||||
|
@ -1464,6 +1464,28 @@ class SessionTest(BaseTestCase):
|
||||
# Should be the same as before r2.
|
||||
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):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user