mirror of
https://github.com/httpie/cli.git
synced 2024-11-22 07:43:20 +01:00
Converted built-in formatters to formatter plugins.
Still work in progress and the API should be considered private for now.
This commit is contained in:
parent
858555abb5
commit
e4c68063b9
@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans.
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__version__ = '0.9.0dev'
|
__version__ = '0.9.0-dev'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,18 +8,18 @@ from textwrap import dedent, wrap
|
|||||||
from argparse import (RawDescriptionHelpFormatter, FileType,
|
from argparse import (RawDescriptionHelpFormatter, FileType,
|
||||||
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
|
OPTIONAL, ZERO_OR_MORE, SUPPRESS)
|
||||||
|
|
||||||
from . import __doc__
|
from httpie import __doc__, __version__
|
||||||
from . import __version__
|
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||||
from .plugins.builtin import BuiltinAuthPlugin
|
from httpie.plugins import plugin_manager
|
||||||
from .plugins import plugin_manager
|
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||||
from .sessions import DEFAULT_SESSIONS_DIR
|
from httpie.output.formatters.colors import AVAILABLE_STYLES, DEFAULT_STYLE
|
||||||
from .output.processors.colors import AVAILABLE_STYLES, DEFAULT_STYLE
|
from httpie.input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
||||||
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
|
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_ITEMS,
|
||||||
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ALL_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, OUTPUT_OPTIONS_DEFAULT,
|
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||||
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
||||||
readable_file_arg)
|
readable_file_arg)
|
||||||
|
|
||||||
|
|
||||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||||
@ -60,7 +60,7 @@ parser = Parser(
|
|||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
positional = parser.add_argument_group(
|
positional = parser.add_argument_group(
|
||||||
title='Positional arguments',
|
title='Positional Arguments',
|
||||||
description=dedent("""
|
description=dedent("""
|
||||||
These arguments come after any flags and in the order they are listed here.
|
These arguments come after any flags and in the order they are listed here.
|
||||||
Only URL is required.
|
Only URL is required.
|
||||||
@ -147,7 +147,7 @@ positional.add_argument(
|
|||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
content_type = parser.add_argument_group(
|
content_type = parser.add_argument_group(
|
||||||
title='Predefined content types',
|
title='Predefined Content Types',
|
||||||
description=None
|
description=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ content_type.add_argument(
|
|||||||
# Output processing
|
# Output processing
|
||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
output_processing = parser.add_argument_group(title='Output processing')
|
output_processing = parser.add_argument_group(title='Output Processing')
|
||||||
|
|
||||||
output_processing.add_argument(
|
output_processing.add_argument(
|
||||||
'--pretty',
|
'--pretty',
|
||||||
@ -208,14 +208,12 @@ output_processing.add_argument(
|
|||||||
environment variable is set to "xterm-256color" or similar
|
environment variable is set to "xterm-256color" or similar
|
||||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||||
|
|
||||||
"""
|
""".format(
|
||||||
.format(
|
|
||||||
default=DEFAULT_STYLE,
|
default=DEFAULT_STYLE,
|
||||||
available='\n'.join(
|
available='\n'.join(
|
||||||
'{0: >20}'.format(line.strip())
|
'{0}{1}'.format(8*' ', line.strip())
|
||||||
for line in
|
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||||
wrap(' '.join(sorted(AVAILABLE_STYLES)), 60)
|
).rstrip(),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -223,7 +221,7 @@ output_processing.add_argument(
|
|||||||
#######################################################################
|
#######################################################################
|
||||||
# Output options
|
# Output options
|
||||||
#######################################################################
|
#######################################################################
|
||||||
output_options = parser.add_argument_group(title='Output options')
|
output_options = parser.add_argument_group(title='Output Options')
|
||||||
|
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--print', '-p',
|
'--print', '-p',
|
||||||
|
@ -4,10 +4,10 @@ from pprint import pformat
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import sessions
|
from httpie import sessions
|
||||||
from . import __version__
|
from httpie import __version__
|
||||||
from .compat import str
|
from httpie.compat import str
|
||||||
from .plugins import plugin_manager
|
from httpie.plugins import plugin_manager
|
||||||
|
|
||||||
|
|
||||||
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Python 2/3 compatibility.
|
Python 2.6, 2.7, and 3.x compatibility.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Borrow these from requests:
|
||||||
#noinspection PyUnresolvedReferences
|
#noinspection PyUnresolvedReferences
|
||||||
from requests.compat import (
|
from requests.compat import is_windows, bytes, str, is_py3, is_py26
|
||||||
is_windows,
|
|
||||||
bytes,
|
|
||||||
str,
|
|
||||||
is_py3,
|
|
||||||
is_py26,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#noinspection PyUnresolvedReferences,PyCompatibility
|
#noinspection PyUnresolvedReferences,PyCompatibility
|
||||||
@ -25,7 +20,6 @@ except ImportError:
|
|||||||
#noinspection PyCompatibility
|
#noinspection PyCompatibility
|
||||||
from urllib2 import urlopen
|
from urllib2 import urlopen
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -2,8 +2,8 @@ import os
|
|||||||
import json
|
import json
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
from . import __version__
|
from httpie import __version__
|
||||||
from .compat import is_windows
|
from httpie.compat import is_windows
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = os.environ.get(
|
DEFAULT_CONFIG_DIR = os.environ.get(
|
||||||
|
@ -72,10 +72,9 @@ def main(args=sys.argv[1:], env=Environment()):
|
|||||||
Return exit status code.
|
Return exit status code.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args = decode_args(args, env.stdin_encoding)
|
from httpie.cli import parser
|
||||||
|
|
||||||
plugin_manager.load_installed_plugins()
|
plugin_manager.load_installed_plugins()
|
||||||
from .cli import parser
|
|
||||||
|
|
||||||
if env.config.default_options:
|
if env.config.default_options:
|
||||||
args = env.config.default_options + args
|
args = env.config.default_options + args
|
||||||
|
@ -10,14 +10,13 @@ import getpass
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
#noinspection PyCompatibility
|
#noinspection PyCompatibility
|
||||||
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||||
from .compat import OrderedDict
|
|
||||||
|
|
||||||
# TODO: Use MultiDict for headers once added to `requests`.
|
# TODO: Use MultiDict for headers once added to `requests`.
|
||||||
# https://github.com/jakubroztocil/httpie/issues/130
|
# https://github.com/jakubroztocil/httpie/issues/130
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from .compat import urlsplit, str
|
from httpie.compat import OrderedDict, urlsplit, str
|
||||||
from .sessions import VALID_SESSION_NAME_PATTERN
|
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||||
|
|
||||||
|
|
||||||
HTTP_POST = 'POST'
|
HTTP_POST = 'POST'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .compat import urlsplit, str
|
from httpie.compat import urlsplit, str
|
||||||
|
|
||||||
|
|
||||||
class HTTPMessage(object):
|
class HTTPMessage(object):
|
||||||
|
0
httpie/output/formatters/__init__.py
Normal file
0
httpie/output/formatters/__init__.py
Normal file
@ -8,7 +8,7 @@ from pygments.formatters.terminal256 import Terminal256Formatter
|
|||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
from httpie.compat import is_windows
|
from httpie.compat import is_windows
|
||||||
from .base import BaseProcessor
|
from httpie.plugins import FormatterPlugin
|
||||||
|
|
||||||
|
|
||||||
# Colors on Windows via colorama don't look that
|
# Colors on Windows via colorama don't look that
|
||||||
@ -18,6 +18,52 @@ AVAILABLE_STYLES.add('solarized')
|
|||||||
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
|
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
|
||||||
|
|
||||||
|
|
||||||
|
class ColorFormatter(FormatterPlugin):
|
||||||
|
"""
|
||||||
|
Colorize using Pygments
|
||||||
|
|
||||||
|
This processor that applies syntax highlighting to the headers,
|
||||||
|
and also to the body if its content type is recognized.
|
||||||
|
|
||||||
|
"""
|
||||||
|
group_name = 'colors'
|
||||||
|
|
||||||
|
def __init__(self, env, color_scheme=DEFAULT_STYLE, **kwargs):
|
||||||
|
super(ColorFormatter, self).__init__(**kwargs)
|
||||||
|
if not env.colors:
|
||||||
|
self.enabled = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cache to speed things up when we process streamed body by line.
|
||||||
|
self.lexer_cache = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
style_class = pygments.styles.get_style_by_name(color_scheme)
|
||||||
|
except ClassNotFound:
|
||||||
|
style_class = Solarized256Style
|
||||||
|
|
||||||
|
if env.is_windows or env.colors == 256:
|
||||||
|
fmt_class = Terminal256Formatter
|
||||||
|
else:
|
||||||
|
fmt_class = TerminalFormatter
|
||||||
|
self.formatter = fmt_class(style=style_class)
|
||||||
|
|
||||||
|
def format_headers(self, headers):
|
||||||
|
return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
|
||||||
|
|
||||||
|
def format_body(self, body, mime):
|
||||||
|
lexer = self.get_lexer(mime)
|
||||||
|
if lexer:
|
||||||
|
body = pygments.highlight(body, lexer, self.formatter)
|
||||||
|
return body.strip()
|
||||||
|
|
||||||
|
def get_lexer(self, mime):
|
||||||
|
if mime in self.lexer_cache:
|
||||||
|
return self.lexer_cache[mime]
|
||||||
|
self.lexer_cache[mime] = get_lexer(mime)
|
||||||
|
return self.lexer_cache[mime]
|
||||||
|
|
||||||
|
|
||||||
def get_lexer(mime):
|
def get_lexer(mime):
|
||||||
mime_types, lexer_names = [mime], []
|
mime_types, lexer_names = [mime], []
|
||||||
type_, subtype = mime.split('/')
|
type_, subtype = mime.split('/')
|
||||||
@ -46,52 +92,6 @@ def get_lexer(mime):
|
|||||||
return lexer
|
return lexer
|
||||||
|
|
||||||
|
|
||||||
class PygmentsProcessor(BaseProcessor):
|
|
||||||
"""
|
|
||||||
Colorize using Pygments
|
|
||||||
|
|
||||||
This processor that applies syntax highlighting to the headers,
|
|
||||||
and also to the body if its content type is recognized.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(PygmentsProcessor, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if not self.env.colors:
|
|
||||||
self.enabled = False
|
|
||||||
return
|
|
||||||
|
|
||||||
# Cache to speed things up when we process streamed body by line.
|
|
||||||
self.lexers_by_type = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
style = pygments.styles.get_style_by_name(
|
|
||||||
self.kwargs.get('pygments_style', DEFAULT_STYLE))
|
|
||||||
except ClassNotFound:
|
|
||||||
style = Solarized256Style
|
|
||||||
|
|
||||||
if self.env.is_windows or self.env.colors == 256:
|
|
||||||
fmt_class = Terminal256Formatter
|
|
||||||
else:
|
|
||||||
fmt_class = TerminalFormatter
|
|
||||||
self.formatter = fmt_class(style=style)
|
|
||||||
|
|
||||||
def process_headers(self, headers):
|
|
||||||
return pygments.highlight(headers, HTTPLexer(), self.formatter).strip()
|
|
||||||
|
|
||||||
def process_body(self, body, mime):
|
|
||||||
lexer = self.get_lexer(mime)
|
|
||||||
if lexer:
|
|
||||||
body = pygments.highlight(body, lexer, self.formatter)
|
|
||||||
return body.strip()
|
|
||||||
|
|
||||||
def get_lexer(self, mime):
|
|
||||||
if mime in self.lexers_by_type:
|
|
||||||
return self.lexers_by_type[mime]
|
|
||||||
self.lexers_by_type[mime] = get_lexer(mime)
|
|
||||||
return self.lexers_by_type[mime]
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPLexer(pygments.lexer.RegexLexer):
|
class HTTPLexer(pygments.lexer.RegexLexer):
|
||||||
"""Simplified HTTP lexer for Pygments.
|
"""Simplified HTTP lexer for Pygments.
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
from .base import BaseProcessor
|
from httpie.plugins import FormatterPlugin
|
||||||
|
|
||||||
|
|
||||||
class HeadersProcessor(BaseProcessor):
|
class HeadersFormatter(FormatterPlugin):
|
||||||
|
|
||||||
def process_headers(self, headers):
|
def format_headers(self, headers):
|
||||||
"""
|
"""
|
||||||
Sorts headers by name while retaining relative
|
Sorts headers by name while retaining relative
|
||||||
order of multiple headers with the same name.
|
order of multiple headers with the same name.
|
@ -1,12 +1,15 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .base import BaseProcessor, DEFAULT_INDENT
|
from httpie.plugins import FormatterPlugin
|
||||||
|
|
||||||
|
|
||||||
class JSONProcessor(BaseProcessor):
|
DEFAULT_INDENT = 4
|
||||||
|
|
||||||
def process_body(self, body, mime):
|
|
||||||
|
class JSONFormatter(FormatterPlugin):
|
||||||
|
|
||||||
|
def format_body(self, body, mime):
|
||||||
if 'json' in mime:
|
if 'json' in mime:
|
||||||
try:
|
try:
|
||||||
obj = json.loads(body)
|
obj = json.loads(body)
|
@ -2,13 +2,16 @@ from __future__ import absolute_import
|
|||||||
import re
|
import re
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
from .base import BaseProcessor, DEFAULT_INDENT
|
from httpie.plugins import FormatterPlugin
|
||||||
|
|
||||||
|
|
||||||
DECLARATION_RE = re.compile('<\?xml[^\n]+?\?>', flags=re.I)
|
DECLARATION_RE = re.compile('<\?xml[^\n]+?\?>', flags=re.I)
|
||||||
DOCTYPE_RE = re.compile('<!DOCTYPE[^\n]+?>', flags=re.I)
|
DOCTYPE_RE = re.compile('<!DOCTYPE[^\n]+?>', flags=re.I)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_INDENT = 4
|
||||||
|
|
||||||
|
|
||||||
def indent(elem, indent_text=' ' * DEFAULT_INDENT):
|
def indent(elem, indent_text=' ' * DEFAULT_INDENT):
|
||||||
"""
|
"""
|
||||||
In-place prettyprint formatter
|
In-place prettyprint formatter
|
||||||
@ -33,10 +36,10 @@ def indent(elem, indent_text=' ' * DEFAULT_INDENT):
|
|||||||
return _indent(elem)
|
return _indent(elem)
|
||||||
|
|
||||||
|
|
||||||
class XMLProcessor(BaseProcessor):
|
class XMLFormatter(FormatterPlugin):
|
||||||
# TODO: tests
|
# TODO: tests
|
||||||
|
|
||||||
def process_body(self, body, mime):
|
def format_body(self, body, mime):
|
||||||
if 'xml' in mime:
|
if 'xml' in mime:
|
||||||
# FIXME: orig NS names get forgotten during the conversion, etc.
|
# FIXME: orig NS names get forgotten during the conversion, etc.
|
||||||
try:
|
try:
|
50
httpie/output/processing.py
Normal file
50
httpie/output/processing.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from httpie.plugins import plugin_manager
|
||||||
|
from httpie.context import Environment
|
||||||
|
|
||||||
|
|
||||||
|
MIME_RE = re.compile(r'^[^/]+/[^/]+$')
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_mime(mime):
|
||||||
|
return mime and MIME_RE.match(mime)
|
||||||
|
|
||||||
|
|
||||||
|
class Conversion(object):
|
||||||
|
|
||||||
|
def get_converter(self, mime):
|
||||||
|
if is_valid_mime(mime):
|
||||||
|
for converter_class in plugin_manager.get_converters():
|
||||||
|
if converter_class.supports(mime):
|
||||||
|
return converter_class(mime)
|
||||||
|
|
||||||
|
|
||||||
|
class Formatting(object):
|
||||||
|
"""A delegate class that invokes the actual processors."""
|
||||||
|
|
||||||
|
def __init__(self, groups, env=Environment(), **kwargs):
|
||||||
|
"""
|
||||||
|
:param groups: names of processor groups to be applied
|
||||||
|
:param env: Environment
|
||||||
|
:param kwargs: additional keyword arguments for processors
|
||||||
|
|
||||||
|
"""
|
||||||
|
available_plugins = plugin_manager.get_formatters_grouped()
|
||||||
|
self.enabled_plugins = []
|
||||||
|
for group in groups:
|
||||||
|
for cls in available_plugins[group]:
|
||||||
|
p = cls(env=env, **kwargs)
|
||||||
|
if p.enabled:
|
||||||
|
self.enabled_plugins.append(p)
|
||||||
|
|
||||||
|
def format_headers(self, headers):
|
||||||
|
for p in self.enabled_plugins:
|
||||||
|
headers = p.format_headers(headers)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def format_body(self, content, mime):
|
||||||
|
if is_valid_mime(mime):
|
||||||
|
for p in self.enabled_plugins:
|
||||||
|
content = p.format_body(content, mime)
|
||||||
|
return content
|
@ -1,44 +0,0 @@
|
|||||||
from httpie.context import Environment
|
|
||||||
from .headers import HeadersProcessor
|
|
||||||
from .json import JSONProcessor
|
|
||||||
from .xml import XMLProcessor
|
|
||||||
from .colors import PygmentsProcessor
|
|
||||||
|
|
||||||
|
|
||||||
class ProcessorManager(object):
|
|
||||||
"""A delegate class that invokes the actual processors."""
|
|
||||||
|
|
||||||
available = {
|
|
||||||
'format': [
|
|
||||||
HeadersProcessor,
|
|
||||||
JSONProcessor,
|
|
||||||
XMLProcessor
|
|
||||||
],
|
|
||||||
'colors': [
|
|
||||||
PygmentsProcessor
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, groups, env=Environment(), **kwargs):
|
|
||||||
"""
|
|
||||||
:param groups: names of processor groups to be applied
|
|
||||||
:param env: Environment
|
|
||||||
:param kwargs: additional keyword arguments for processors
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.enabled = []
|
|
||||||
for group in groups:
|
|
||||||
for cls in self.available[group]:
|
|
||||||
p = cls(env, **kwargs)
|
|
||||||
if p.enabled:
|
|
||||||
self.enabled.append(p)
|
|
||||||
|
|
||||||
def process_headers(self, headers):
|
|
||||||
for p in self.enabled:
|
|
||||||
headers = p.process_headers(headers)
|
|
||||||
return headers
|
|
||||||
|
|
||||||
def process_body(self, body, mime):
|
|
||||||
for p in self.enabled:
|
|
||||||
body = p.process_body(body, mime)
|
|
||||||
return body
|
|
@ -1,37 +0,0 @@
|
|||||||
from httpie.context import Environment
|
|
||||||
|
|
||||||
|
|
||||||
# The default number of spaces to indent when pretty printing
|
|
||||||
DEFAULT_INDENT = 4
|
|
||||||
|
|
||||||
|
|
||||||
class BaseProcessor(object):
|
|
||||||
"""Base output processor class."""
|
|
||||||
|
|
||||||
def __init__(self, env=Environment(), **kwargs):
|
|
||||||
"""
|
|
||||||
:param env: an class:`Environment` instance
|
|
||||||
:param kwargs: additional keyword argument that some
|
|
||||||
processor might require.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.enabled = True
|
|
||||||
self.env = env
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
def process_headers(self, headers):
|
|
||||||
"""Return processed `headers`
|
|
||||||
|
|
||||||
:param headers: The headers as text.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return headers
|
|
||||||
|
|
||||||
def process_body(self, content, mime):
|
|
||||||
"""Return processed `content`.
|
|
||||||
|
|
||||||
:param content: The body content as text
|
|
||||||
:param mime: E.g., 'application/atom+xml'.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return content
|
|
@ -1,11 +1,12 @@
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from httpie.compat import str
|
||||||
from httpie.context import Environment
|
from httpie.context import Environment
|
||||||
from httpie.models import HTTPRequest, HTTPResponse
|
from httpie.models import HTTPRequest, HTTPResponse
|
||||||
from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||||
from httpie.output.processors import ProcessorManager
|
from httpie.output.processing import Formatting, Conversion
|
||||||
|
|
||||||
|
|
||||||
BINARY_SUPPRESSED_NOTICE = (
|
BINARY_SUPPRESSED_NOTICE = (
|
||||||
@ -59,7 +60,6 @@ def build_output_stream(args, env, request, response):
|
|||||||
exchange each of which yields `bytes` chunks.
|
exchange each of which yields `bytes` chunks.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
req_h = OUT_REQ_HEAD in args.output_options
|
req_h = OUT_REQ_HEAD in args.output_options
|
||||||
req_b = OUT_REQ_BODY in args.output_options
|
req_b = OUT_REQ_BODY in args.output_options
|
||||||
resp_h = OUT_RESP_HEAD in args.output_options
|
resp_h = OUT_RESP_HEAD in args.output_options
|
||||||
@ -111,11 +111,9 @@ def get_stream_type(env, args):
|
|||||||
Stream = partial(
|
Stream = partial(
|
||||||
PrettyStream if args.stream else BufferedPrettyStream,
|
PrettyStream if args.stream else BufferedPrettyStream,
|
||||||
env=env,
|
env=env,
|
||||||
processor=ProcessorManager(
|
conversion=Conversion(),
|
||||||
env=env,
|
formatting=Formatting(env=env, groups=args.prettify,
|
||||||
groups=args.prettify,
|
color_scheme=args.style),
|
||||||
pygments_style=args.style
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
Stream = partial(EncodedStream, env=env)
|
Stream = partial(EncodedStream, env=env)
|
||||||
@ -140,23 +138,23 @@ class BaseStream(object):
|
|||||||
self.with_body = with_body
|
self.with_body = with_body
|
||||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||||
|
|
||||||
def _get_headers(self):
|
def get_headers(self):
|
||||||
"""Return the headers' bytes."""
|
"""Return the headers' bytes."""
|
||||||
return self.msg.headers.encode('utf8')
|
return self.msg.headers.encode('utf8')
|
||||||
|
|
||||||
def _iter_body(self):
|
def iter_body(self):
|
||||||
"""Return an iterator over the message body."""
|
"""Return an iterator over the message body."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Return an iterator over `self.msg`."""
|
"""Return an iterator over `self.msg`."""
|
||||||
if self.with_headers:
|
if self.with_headers:
|
||||||
yield self._get_headers()
|
yield self.get_headers()
|
||||||
yield b'\r\n\r\n'
|
yield b'\r\n\r\n'
|
||||||
|
|
||||||
if self.with_body:
|
if self.with_body:
|
||||||
try:
|
try:
|
||||||
for chunk in self._iter_body():
|
for chunk in self.iter_body():
|
||||||
yield chunk
|
yield chunk
|
||||||
if self.on_body_chunk_downloaded:
|
if self.on_body_chunk_downloaded:
|
||||||
self.on_body_chunk_downloaded(chunk)
|
self.on_body_chunk_downloaded(chunk)
|
||||||
@ -176,7 +174,7 @@ class RawStream(BaseStream):
|
|||||||
super(RawStream, self).__init__(**kwargs)
|
super(RawStream, self).__init__(**kwargs)
|
||||||
self.chunk_size = chunk_size
|
self.chunk_size = chunk_size
|
||||||
|
|
||||||
def _iter_body(self):
|
def iter_body(self):
|
||||||
return self.msg.iter_body(self.chunk_size)
|
return self.msg.iter_body(self.chunk_size)
|
||||||
|
|
||||||
|
|
||||||
@ -204,7 +202,7 @@ class EncodedStream(BaseStream):
|
|||||||
# Default to utf8 when unsure.
|
# Default to utf8 when unsure.
|
||||||
self.output_encoding = output_encoding or 'utf8'
|
self.output_encoding = output_encoding or 'utf8'
|
||||||
|
|
||||||
def _iter_body(self):
|
def iter_body(self):
|
||||||
|
|
||||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||||
|
|
||||||
@ -226,25 +224,44 @@ class PrettyStream(EncodedStream):
|
|||||||
|
|
||||||
CHUNK_SIZE = 1
|
CHUNK_SIZE = 1
|
||||||
|
|
||||||
def __init__(self, processor, **kwargs):
|
def __init__(self, conversion, formatting, **kwargs):
|
||||||
super(PrettyStream, self).__init__(**kwargs)
|
super(PrettyStream, self).__init__(**kwargs)
|
||||||
self.processor = processor
|
self.formatting = formatting
|
||||||
|
self.conversion = conversion
|
||||||
|
self.mime = self.msg.content_type.split(';')[0]
|
||||||
|
|
||||||
def _get_headers(self):
|
def get_headers(self):
|
||||||
return self.processor.process_headers(
|
return self.formatting.format_headers(
|
||||||
self.msg.headers).encode(self.output_encoding)
|
self.msg.headers).encode(self.output_encoding)
|
||||||
|
|
||||||
def _iter_body(self):
|
def iter_body(self):
|
||||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
first_chunk = True
|
||||||
|
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
||||||
|
for line, lf in iter_lines:
|
||||||
if b'\0' in line:
|
if b'\0' in line:
|
||||||
|
if first_chunk:
|
||||||
|
converter = self.conversion.get_converter(self.mime)
|
||||||
|
if converter:
|
||||||
|
body = bytearray()
|
||||||
|
# noinspection PyAssignmentToLoopOrWithParameter
|
||||||
|
for line, lf in chain([(line, lf)], iter_lines):
|
||||||
|
body.extend(line)
|
||||||
|
body.extend(lf)
|
||||||
|
self.mime, body = converter.convert(body)
|
||||||
|
assert isinstance(body, str)
|
||||||
|
yield self.process_body(body)
|
||||||
|
return
|
||||||
raise BinarySuppressedError()
|
raise BinarySuppressedError()
|
||||||
yield self._process_body(line) + lf
|
yield self.process_body(line) + lf
|
||||||
|
first_chunk = False
|
||||||
|
|
||||||
def _process_body(self, chunk):
|
def process_body(self, chunk):
|
||||||
return self.processor.process_body(
|
if not isinstance(chunk, str):
|
||||||
body=chunk.decode(self.msg.encoding, 'replace'),
|
# Text when a converter has been used,
|
||||||
mime=self.msg.content_type.split(';')[0]
|
# otherwise it will always be bytes.
|
||||||
).encode(self.output_encoding, 'replace')
|
chunk = chunk.decode(self.msg.encoding, 'replace')
|
||||||
|
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
|
||||||
|
return chunk.encode(self.output_encoding, 'replace')
|
||||||
|
|
||||||
|
|
||||||
class BufferedPrettyStream(PrettyStream):
|
class BufferedPrettyStream(PrettyStream):
|
||||||
@ -257,14 +274,20 @@ class BufferedPrettyStream(PrettyStream):
|
|||||||
|
|
||||||
CHUNK_SIZE = 1024 * 10
|
CHUNK_SIZE = 1024 * 10
|
||||||
|
|
||||||
def _iter_body(self):
|
def iter_body(self):
|
||||||
|
|
||||||
# Read the whole body before prettifying it,
|
# Read the whole body before prettifying it,
|
||||||
# but bail out immediately if the body is binary.
|
# but bail out immediately if the body is binary.
|
||||||
|
converter = None
|
||||||
body = bytearray()
|
body = bytearray()
|
||||||
|
|
||||||
for chunk in self.msg.iter_body(self.CHUNK_SIZE):
|
for chunk in self.msg.iter_body(self.CHUNK_SIZE):
|
||||||
if b'\0' in chunk:
|
if not converter and b'\0' in chunk:
|
||||||
raise BinarySuppressedError()
|
converter = self.conversion.get_converter(self.mime)
|
||||||
|
if not converter:
|
||||||
|
raise BinarySuppressedError()
|
||||||
body.extend(chunk)
|
body.extend(chunk)
|
||||||
|
|
||||||
yield self._process_body(body)
|
if converter:
|
||||||
|
self.mime, body = converter.convert(body)
|
||||||
|
|
||||||
|
yield self.process_body(body)
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
from .base import AuthPlugin
|
from httpie.plugins.base import AuthPlugin, FormatterPlugin, ConverterPlugin
|
||||||
from .manager import PluginManager
|
from httpie.plugins.manager import PluginManager
|
||||||
from .builtin import BasicAuthPlugin, DigestAuthPlugin
|
from httpie.plugins.builtin import BasicAuthPlugin, DigestAuthPlugin
|
||||||
|
from httpie.output.formatters.headers import HeadersFormatter
|
||||||
|
from httpie.output.formatters.json import JSONFormatter
|
||||||
|
from httpie.output.formatters.xml import XMLFormatter
|
||||||
|
from httpie.output.formatters.colors import ColorFormatter
|
||||||
|
|
||||||
|
|
||||||
plugin_manager = PluginManager()
|
plugin_manager = PluginManager()
|
||||||
plugin_manager.register(BasicAuthPlugin)
|
plugin_manager.register(BasicAuthPlugin,
|
||||||
plugin_manager.register(DigestAuthPlugin)
|
DigestAuthPlugin)
|
||||||
|
plugin_manager.register(HeadersFormatter,
|
||||||
|
JSONFormatter,
|
||||||
|
XMLFormatter,
|
||||||
|
ColorFormatter)
|
||||||
|
@ -1,14 +1,4 @@
|
|||||||
class AuthPlugin(object):
|
class BasePlugin(object):
|
||||||
"""
|
|
||||||
Base auth plugin class.
|
|
||||||
|
|
||||||
See <https://github.com/jakubroztocil/httpie-ntlm> for an example auth plugin.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The value that should be passed to --auth-type
|
|
||||||
# to use this auth plugin. Eg. "my-auth"
|
|
||||||
auth_type = None
|
|
||||||
|
|
||||||
# The name of the plugin, eg. "My auth".
|
# The name of the plugin, eg. "My auth".
|
||||||
name = None
|
name = None
|
||||||
@ -20,9 +10,64 @@ class AuthPlugin(object):
|
|||||||
# This be set automatically once the plugin has been loaded.
|
# This be set automatically once the plugin has been loaded.
|
||||||
package_name = None
|
package_name = None
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPlugin(BasePlugin):
|
||||||
|
"""
|
||||||
|
Base auth plugin class.
|
||||||
|
|
||||||
|
See <https://github.com/jkbr/httpie-ntlm> for an example auth plugin.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# The value that should be passed to --auth-type
|
||||||
|
# to use this auth plugin. Eg. "my-auth"
|
||||||
|
auth_type = None
|
||||||
|
|
||||||
def get_auth(self, username, password):
|
def get_auth(self, username, password):
|
||||||
"""
|
"""
|
||||||
Return a ``requests.auth.AuthBase`` subclass instance.
|
Return a ``requests.auth.AuthBase`` subclass instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class ConverterPlugin(object):
|
||||||
|
|
||||||
|
def __init__(self, mime):
|
||||||
|
self.mime = mime
|
||||||
|
|
||||||
|
def convert(self, content_bytes):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def supports(cls, mime):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class FormatterPlugin(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
:param env: an class:`Environment` instance
|
||||||
|
:param kwargs: additional keyword argument that some
|
||||||
|
processor might require.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.enabled = True
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
def format_headers(self, headers):
|
||||||
|
"""Return processed `headers`
|
||||||
|
|
||||||
|
:param headers: The headers as text.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def format_body(self, content, mime):
|
||||||
|
"""Return processed `content`.
|
||||||
|
|
||||||
|
:param mime: E.g., 'application/atom+xml'.
|
||||||
|
:param content: The body content as text
|
||||||
|
|
||||||
|
"""
|
||||||
|
return content
|
||||||
|
@ -2,7 +2,7 @@ from base64 import b64encode
|
|||||||
|
|
||||||
import requests.auth
|
import requests.auth
|
||||||
|
|
||||||
from .base import AuthPlugin
|
from httpie.plugins.base import AuthPlugin
|
||||||
|
|
||||||
|
|
||||||
class BuiltinAuthPlugin(AuthPlugin):
|
class BuiltinAuthPlugin(AuthPlugin):
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
|
from itertools import groupby
|
||||||
from pkg_resources import iter_entry_points
|
from pkg_resources import iter_entry_points
|
||||||
|
from httpie.plugins import AuthPlugin, FormatterPlugin, ConverterPlugin
|
||||||
|
|
||||||
|
|
||||||
ENTRY_POINT_NAMES = [
|
ENTRY_POINT_NAMES = [
|
||||||
'httpie.plugins.auth.v1'
|
'httpie.plugins.auth.v1',
|
||||||
|
'httpie.plugins.formatter.v1',
|
||||||
|
'httpie.plugins.converter.v1',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -14,22 +18,41 @@ class PluginManager(object):
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self._plugins)
|
return iter(self._plugins)
|
||||||
|
|
||||||
def register(self, plugin):
|
def register(self, *plugins):
|
||||||
self._plugins.append(plugin)
|
for plugin in plugins:
|
||||||
|
self._plugins.append(plugin)
|
||||||
def get_auth_plugins(self):
|
|
||||||
return list(self._plugins)
|
|
||||||
|
|
||||||
def get_auth_plugin_mapping(self):
|
|
||||||
return dict((plugin.auth_type, plugin) for plugin in self)
|
|
||||||
|
|
||||||
def get_auth_plugin(self, auth_type):
|
|
||||||
return self.get_auth_plugin_mapping()[auth_type]
|
|
||||||
|
|
||||||
def load_installed_plugins(self):
|
def load_installed_plugins(self):
|
||||||
|
|
||||||
for entry_point_name in ENTRY_POINT_NAMES:
|
for entry_point_name in ENTRY_POINT_NAMES:
|
||||||
for entry_point in iter_entry_points(entry_point_name):
|
for entry_point in iter_entry_points(entry_point_name):
|
||||||
plugin = entry_point.load()
|
plugin = entry_point.load()
|
||||||
plugin.package_name = entry_point.dist.key
|
plugin.package_name = entry_point.dist.key
|
||||||
self.register(entry_point.load())
|
self.register(entry_point.load())
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
def get_auth_plugins(self):
|
||||||
|
return [plugin for plugin in self if issubclass(plugin, AuthPlugin)]
|
||||||
|
|
||||||
|
def get_auth_plugin_mapping(self):
|
||||||
|
return dict((plugin.auth_type, plugin)
|
||||||
|
for plugin in self.get_auth_plugins())
|
||||||
|
|
||||||
|
def get_auth_plugin(self, auth_type):
|
||||||
|
return self.get_auth_plugin_mapping()[auth_type]
|
||||||
|
|
||||||
|
# Output processing
|
||||||
|
def get_formatters(self):
|
||||||
|
return [plugin for plugin in self
|
||||||
|
if issubclass(plugin, FormatterPlugin)]
|
||||||
|
|
||||||
|
def get_formatters_grouped(self):
|
||||||
|
groups = {}
|
||||||
|
for group_name, group in groupby(
|
||||||
|
self.get_formatters(),
|
||||||
|
key=lambda p: getattr(p, 'group_name', 'format')):
|
||||||
|
groups[group_name] = list(group)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def get_converters(self):
|
||||||
|
return [plugin for plugin in self
|
||||||
|
if issubclass(plugin, ConverterPlugin)]
|
||||||
|
@ -7,8 +7,8 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
from requests.cookies import RequestsCookieJar, create_cookie
|
from requests.cookies import RequestsCookieJar, create_cookie
|
||||||
|
|
||||||
from .compat import urlsplit
|
from httpie.compat import urlsplit
|
||||||
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||||
from httpie.plugins import plugin_manager
|
from httpie.plugins import plugin_manager
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from httpie import ExitStatus
|
from httpie import ExitStatus
|
||||||
from httpie.output.processors.colors import get_lexer
|
from httpie.output.formatters.colors import get_lexer
|
||||||
from utils import TestEnvironment, http, httpbin, HTTP_OK, COLOR, CRLF
|
from utils import TestEnvironment, http, httpbin, HTTP_OK, COLOR, CRLF
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,14 +44,14 @@ class TestUnicode:
|
|||||||
def test_unicode_raw_json_item(self):
|
def test_unicode_raw_json_item(self):
|
||||||
r = http('--json', 'POST', httpbin('/post'),
|
r = http('--json', 'POST', httpbin('/post'),
|
||||||
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
|
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
|
||||||
|
assert HTTP_OK in r
|
||||||
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
|
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
|
||||||
|
|
||||||
def test_unicode_raw_json_item_verbose(self):
|
def test_unicode_raw_json_item_verbose(self):
|
||||||
r = http('--verbose',
|
r = http('--json', 'POST', httpbin('/post'),
|
||||||
'--json', 'POST', httpbin('/post'),
|
|
||||||
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
|
u'test:={ "%s" : [ "%s" ] }' % (UNICODE, UNICODE))
|
||||||
assert HTTP_OK in r
|
assert HTTP_OK in r
|
||||||
assert UNICODE in r
|
assert r.json['json'] == {'test': {UNICODE: [UNICODE]}}
|
||||||
|
|
||||||
def test_unicode_url_query_arg_item(self):
|
def test_unicode_url_query_arg_item(self):
|
||||||
r = http(httpbin('/get'), u'test==%s' % UNICODE)
|
r = http(httpbin('/get'), u'test==%s' % UNICODE)
|
||||||
|
@ -158,7 +158,6 @@ def http(*args, **kwargs):
|
|||||||
stdout.seek(0)
|
stdout.seek(0)
|
||||||
stderr.seek(0)
|
stderr.seek(0)
|
||||||
output = stdout.read()
|
output = stdout.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
output = output.decode('utf8')
|
output = output.decode('utf8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
|
Loading…
Reference in New Issue
Block a user