Permalink
Browse files

Added anonymous sessions (--session=/file/path.json).

  • Loading branch information...
jakubroztocil committed May 13, 2013
1 parent 76eebea commit 87c59ae561a6a7c1e0e64befbb1c4420df8561d1
Showing with 108 additions and 116 deletions.
  1. +27 −5 README.rst
  2. +14 −9 httpie/cli.py
  3. +1 −1 httpie/client.py
  4. +13 −8 httpie/config.py
  5. +6 −14 httpie/input.py
  6. +25 −79 httpie/sessions.py
  7. +22 −0 tests/tests.py
View
@@ -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.
View
@@ -7,13 +7,13 @@
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 @@ def _(text):
''')
)
+
###############################################################################
# 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 @@ def _(text):
)
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 @@ def _(text):
###############################################################################
# Authentication
###############################################################################
+
# ``requests.request`` keyword arguments.
auth = parser.add_argument_group(title='Authentication')
auth.add_argument(
@@ -312,8 +316,9 @@ def _(text):
)
+###############################################################################
# Network
-#############################################
+###############################################################################
network = parser.add_argument_group(title='Network')
View
@@ -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),
)
View
@@ -16,9 +16,8 @@
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 @@ def __init__(self, directory=None, *args, **kwargs):
def __getattr__(self, item):
return self[item]
+ 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(self.directory, mode=0o700)
+ os.makedirs(os.path.dirname(path), mode=0o700)
except OSError as e:
if e.errno != errno.EEXIST:
raise
- return os.path.join(self.directory, self.name + '.json')
+ 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 @@ def save(self):
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 @@ def delete(self):
class Config(BaseConfigDict):
name = 'config'
- help = 'https://github.com/jkbr/httpie#config'
+ helpurl = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file'
DEFAULTS = {
View
@@ -20,6 +20,7 @@
from requests.structures import CaseInsensitiveDict
from .compat import urlsplit, str
+from .sessions import VALID_SESSION_NAME_PATTERN
HTTP_POST = 'POST'
@@ -373,24 +374,15 @@ def __eq__(self, other):
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
View
@@ -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 @@
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 @@ def __init__(self, host, name, *args, **kwargs):
'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):
"""
Oops, something went wrong.

0 comments on commit 87c59ae

Please sign in to comment.