diff --git a/README.rst b/README.rst index b2be5aee..9470ebf2 100644 --- a/README.rst +++ b/README.rst @@ -919,15 +919,20 @@ following keys: For instance, you can use this option to change the default style and output options: ``"default_options": ["--style=fruity", "--body"]`` + + Another useful default option is + ``"--session=default"`` to make HTTPie always + use `sessions`_. + Default options from config file can be unset - via ``--no-OPTION`` arguments passed on the - command line (e.g., ``--no-style`` would unset - ``fruity`` as the default style for the - particular invocation). + for a particular invocation via + ``--no-OPTION`` arguments passed on the + command line (e.g., ``--no-style`` + or ``--no-session``). ========================= ================================================= -The default location is ``~/.httpie/config.json`` -(``%APPDATA%\httpie\config.json`` on Windows). +The default location of the configuration file is ``~/.httpie/config.json`` +(or ``%APPDATA%\httpie\config.json`` on Windows). The config directory location can be changed by setting the ``HTTPIE_CONFIG_DIR`` environment variable. @@ -942,10 +947,17 @@ HTTP requests. The ``httpie`` command, on the other hand, is a utility for managing your configuration. The currently supported actions are: -* ``httpie session list [hostname]`` -* ``httpie session edit hostname session-name`` -* ``httpie session show hostname session-name`` -* ``httpie session delete hostname [session-name]`` +``httpie session list [hostname]``: + List all existing sessions, or a host's sessions only. + +``httpie session edit hostname session-name``: + Create and/or edit a session file in $EDITOR. + +``httpie session show hostname session-name``: + Print a session data to the console. + +``httpie session delete hostname [session-name]`` + Delete all host's sessions or a specific one by name. ========= diff --git a/httpie/cli.py b/httpie/cli.py index 98edfbba..8192fa53 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -12,9 +12,9 @@ from requests.compat import is_windows from . import __doc__ from . import __version__ from .sessions import DEFAULT_SESSIONS_DIR +from .manage import session_name_validator from .output import AVAILABLE_STYLES, DEFAULT_STYLE -from .input import (Parser, AuthCredentialsArgType, - KeyValueArgType, SessionNameArgType, +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, @@ -225,7 +225,7 @@ sessions = parser.add_argument_group(title='Sessions')\ .add_mutually_exclusive_group(required=False) sessions.add_argument( - '--session', metavar='SESSION_NAME', type=SessionNameArgType(), + '--session', metavar='SESSION_NAME', type=session_name_validator, help=_(''' Create, or reuse and update a session. Within a session, custom headers, auth credential, as well as any diff --git a/httpie/core.py b/httpie/core.py index 3d1e9438..51fb0924 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -26,15 +26,15 @@ from .output import build_output_stream, write, write_with_colors_win_p3k from . import ExitStatus -def get_exist_status_code(code, follow=False): +def get_exit_status(http_status, follow=False): """Translate HTTP status code to exit status code.""" - if 300 <= code <= 399 and not follow: + if 300 <= http_status <= 399 and not follow: # Redirect return ExitStatus.ERROR_HTTP_3XX - elif 400 <= code <= 499: + elif 400 <= http_status <= 499: # Client Error return ExitStatus.ERROR_HTTP_4XX - elif 500 <= code <= 599: + elif 500 <= http_status <= 599: # Server Error return ExitStatus.ERROR_HTTP_5XX else: @@ -67,12 +67,12 @@ def main(args=sys.argv[1:], env=Environment()): debug = '--debug' in args traceback = debug or '--traceback' in args - exit_status_code = ExitStatus.OK + exit_status = ExitStatus.OK if debug: print_debug_info(env) if args == ['--debug']: - return exit_status_code + return exit_status try: args = parser.parse_args(args=args, env=env) @@ -80,18 +80,18 @@ def main(args=sys.argv[1:], env=Environment()): response = get_response(args, config_dir=env.config.directory) if args.check_status: - exit_status_code = get_exist_status_code(response.status_code, - args.follow) + exit_status = get_exit_status(response.status_code, args.follow) - if not env.stdout_isatty and exit_status_code != ExitStatus.OK: + if not env.stdout_isatty and exit_status != ExitStatus.OK: error('HTTP %s %s', response.raw.status, response.raw.reason, level='warning') write_kwargs = { - 'stream': build_output_stream( - args, env, response.request, response), + 'stream': build_output_stream(args, env, + response.request, + response), 'outfile': env.stdout, 'flush': env.stdout_isatty or args.stream } @@ -112,10 +112,10 @@ def main(args=sys.argv[1:], env=Environment()): if traceback: raise env.stderr.write('\n') - exit_status_code = ExitStatus.ERROR + exit_status = ExitStatus.ERROR except requests.Timeout: - exit_status_code = ExitStatus.ERROR_TIMEOUT + exit_status = ExitStatus.ERROR_TIMEOUT error('Request timed out (%ss).', args.timeout) except Exception as e: @@ -124,6 +124,6 @@ def main(args=sys.argv[1:], env=Environment()): if traceback: raise error('%s: %s', type(e).__name__, str(e)) - exit_status_code = ExitStatus.ERROR + exit_status = ExitStatus.ERROR - return exit_status_code + return exit_status diff --git a/httpie/input.py b/httpie/input.py index 5d7d6318..00d859ba 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -8,7 +8,7 @@ import json import mimetypes import getpass from io import BytesIO -from argparse import ArgumentParser, ArgumentTypeError +from argparse import ArgumentParser, ArgumentTypeError, ArgumentError try: from collections import OrderedDict @@ -171,7 +171,6 @@ class Parser(ArgumentParser): msg = 'unrecognized arguments: %s' self.error(msg % ' '.join(invalid)) - def _print_message(self, message, file=None): # Sneak in our stderr/stdout. file = { @@ -308,6 +307,38 @@ 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 + + +def host_name_arg_type(name): + from .sessions import Host + if not Host.is_valid_name(name): + raise ArgumentTypeError( + 'special characters and spaces are not' + ' allowed in host names: "%s"' + % name) + return name + + +class RegexValidator(object): + + def __init__(self, pattern, error_message): + self.pattern = re.compile(pattern) + self.error_message = error_message + + def __call__(self, value): + if not self.pattern.search(value): + raise ArgumentError(None, self.error_message) + return value + + class KeyValueArgType(object): """A key-value pair argument type used with `argparse`. diff --git a/httpie/manage.py b/httpie/manage.py index 4a4265fa..7c94c90a 100644 --- a/httpie/manage.py +++ b/httpie/manage.py @@ -4,11 +4,14 @@ Provides the `httpie' management command. Note that the main `http' command points to `httpie.__main__.main()`. """ +import inspect import argparse -from argparse import OPTIONAL +import functools from . import __version__ -from .sessions import (command_session_list, +from .input import RegexValidator +from .sessions import (Session, Host, + command_session_list, command_session_edit, command_session_show, command_session_delete) @@ -25,34 +28,78 @@ subparsers = parser.add_subparsers() # Session commands ################################################################# +hostname_validator = RegexValidator( + Host.VALID_NAME_PATTERN, + 'Hostname contains invalid characters.' +) +session_name_validator = RegexValidator( + Session.VALID_NAME_PATTERN, + 'Session name contains invalid characters.' +) + + +def make_command(func): + @functools.wraps(func) + def wrapper(parsed_args): + """Convert parsed args to function kwargs.""" + kwargs = dict((name, getattr(parsed_args, name, None)) + for name in inspect.getargspec(func).args) + return func(**kwargs) + return wrapper + + +def add_hostname_arg(parser, *args, **kwargs): + parser.add_argument( + 'hostname', metavar='HOSTNAME', + type=hostname_validator, + *args, **kwargs + ) + + +def add_session_name_arg(parser, *args, **kwargs): + parser.add_argument( + 'session_name', metavar='SESSION_NAME', + type=session_name_validator, + *args, **kwargs + ) + + session = subparsers.add_parser('session', help='manipulate and inspect sessions').add_subparsers() # List -list_ = session.add_parser('list', help='list sessions') -list_.set_defaults(command=command_session_list) -list_.add_argument('host', nargs=OPTIONAL) +session_list_parser = session.add_parser('list', help='list sessions') +session_list_parser.set_defaults(command=make_command(command_session_list)) +add_hostname_arg(session_list_parser, nargs=argparse.OPTIONAL) + # Show -show = session.add_parser('show', help='show a session') -show.set_defaults(command=command_session_show) -show.add_argument('host') -show.add_argument('name') +session_show_parser = session.add_parser('show', help='show a session') +session_show_parser.set_defaults(command=make_command(command_session_show)) +add_hostname_arg(session_show_parser) +add_session_name_arg(session_show_parser) + # Edit -edit = session.add_parser( +session_edit_parser = session.add_parser( 'edit', help='edit a session in $EDITOR') -edit.set_defaults(command=command_session_edit) -edit.add_argument('host') -edit.add_argument('name') +session_edit_parser.set_defaults(command=make_command(command_session_edit)) +add_hostname_arg(session_edit_parser) +add_session_name_arg(session_edit_parser) # Delete -delete = session.add_parser('delete', help='delete a session') -delete.set_defaults(command=command_session_delete) -delete.add_argument('host') -delete.add_argument('name', nargs=OPTIONAL, +session_delete_parser = session.add_parser('delete', help='delete a session') +session_delete_parser.set_defaults( + command=make_command(command_session_delete)) +add_hostname_arg(session_delete_parser) +add_session_name_arg(session_delete_parser, nargs=argparse.OPTIONAL, help='The name of the session to be deleted.' - ' If not specified, all host sessions are deleted.') + ' If not specified, all of the host\'s') + + +################################################################# +# Main +################################################################# def main(): diff --git a/httpie/models.py b/httpie/models.py index 9c67cb90..202504fe 100644 --- a/httpie/models.py +++ b/httpie/models.py @@ -142,7 +142,6 @@ class HTTPRequest(HTTPMessage): @property def headers(self): - """Return Request-Line""" url = urlparse(self._orig.url) # Querystring @@ -172,7 +171,6 @@ class HTTPRequest(HTTPMessage): headers = ['%s: %s' % (name, value) for name, value in headers.items()] - #noinspection PyTypeChecker headers.insert(0, request_line) return '\r\n'.join(headers).strip() diff --git a/httpie/output.py b/httpie/output.py index df961808..dab24842 100644 --- a/httpie/output.py +++ b/httpie/output.py @@ -154,6 +154,7 @@ class BaseStream(object): :param with_body: if `True`, body will be included """ + assert with_headers or with_body self.msg = msg self.with_headers = with_headers self.with_body = with_body diff --git a/httpie/sessions.py b/httpie/sessions.py index 0e70747a..7a28fb9d 100644 --- a/httpie/sessions.py +++ b/httpie/sessions.py @@ -65,19 +65,22 @@ def get_response(name, request_kwargs, config_dir, read_only=False): 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 a iterator yielding `Session` instances.""" + """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) @property def verbose_name(self): - return u'%s %s' % (self.name, self.path) + return '%s %s' % (self.name, self.path) def delete(self): shutil.rmtree(self.path) @@ -111,7 +114,10 @@ class Session(BaseConfigDict): help = '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) super(Session, self).__init__(*args, **kwargs) self.host = host self.name = name @@ -119,8 +125,8 @@ class Session(BaseConfigDict): self['cookies'] = {} self['auth'] = { 'type': None, - 'username': '', - 'password': '' + 'username': None, + 'password': None } @property @@ -129,7 +135,7 @@ class Session(BaseConfigDict): @property def verbose_name(self): - return u'%s %s %s' % (self.host.name, self.name, self.path) + return '%s %s %s' % (self.host.name, self.name, self.path) @property def cookies(self): @@ -161,7 +167,7 @@ class Session(BaseConfigDict): def auth(self): auth = self.get('auth', None) if not auth or not auth['type']: - return None + return Auth = {'basic': HTTPBasicAuth, 'digest': HTTPDigestAuth}[auth['type']] return Auth(auth['username'], auth['password']) @@ -176,21 +182,19 @@ class Session(BaseConfigDict): } - - ################################################################## # Session management commands # TODO: write tests ################################################################## -def command_session_list(args): +def command_session_list(hostname=None): """Print a list of all sessions or only the ones from `args.host`, if provided. """ - if args.host: - for session in Host(args.host): + if hostname: + for session in Host(hostname): print(session.verbose_name) else: for host in Host.all(): @@ -198,9 +202,9 @@ def command_session_list(args): print(session.verbose_name) -def command_session_show(args): - """Print session JSON data for `args.host` and `args.name`.""" - session = Session(Host(args.host), args.name) +def command_session_show(hostname, session_name): + """Print JSON data for a session.""" + session = Session(Host(hostname), session_name) path = session.path if not os.path.exists(path): sys.stderr.write('Session does not exist: %s\n' @@ -214,20 +218,19 @@ def command_session_show(args): print('') -def command_session_delete(args): +def command_session_delete(hostname, session_name=None): """Delete a session by host and name, or delete all the host's session if name not provided. """ - host = Host(args.host) - if not args.name: + host = Host(hostname) + if not session_name: host.delete() - else: - session = Session(host, args.name) + session = Session(host, session_name) session.delete() -def command_session_edit(args): +def command_session_edit(hostname, session_name): """Open a session file in EDITOR.""" editor = os.environ.get('EDITOR', None) if not editor: @@ -235,7 +238,7 @@ def command_session_edit(args): 'You need to configure the environment variable EDITOR.\n') sys.exit(1) - session = Session(Host(args.host), args.name) + session = Session(Host(hostname), session_name) if session.is_new: session.save()