Compare commits

..

123 Commits
0.2.6 ... 0.4.0

Author SHA1 Message Date
d4f2daca56 v0.4.0 2013-02-22 13:52:05 +01:00
d97a610f7c Added new logo by @claudiatd 2013-02-22 13:51:37 +01:00
5cc5b13555 Removed the management command.
It means that:

    httpie session list
    httpie session edit
    ...

are gone.

It has never been part of a stable release, and since it wasn't
a very useful feature, it's beeing removed now to avoid feature creep.
2013-02-22 13:27:26 +01:00
3043f24733 .gitignore 2013-02-22 13:19:18 +01:00
093dab5896 Multiple headers TODO. 2013-02-22 13:18:18 +01:00
5f42a21cfb Simplified stored session cookie data. 2013-01-22 20:03:28 +01:00
4c45f0d91f Session name escaping. 2013-01-22 20:02:39 +01:00
d7ec7b2217 Fixing tests for Travis. 2013-01-04 03:19:38 +01:00
7817dfbbcc Fixing tests for Travis. 2013-01-04 03:09:21 +01:00
238b2e0441 Fixing tests for Travis. 2013-01-04 03:05:36 +01:00
a93d57b58b Fixed request/response session cookies.
Closes #113.
2013-01-04 02:59:05 +01:00
79c412064a Python 3.3 fixes. 2013-01-03 15:19:21 +01:00
0ae9d7af58 Compatibility with requests v1.0.4 (requests URL params). 2013-01-03 14:42:17 +01:00
80e317fe24 Added Python 3.3 to tox and travis conf. 2013-01-03 14:14:22 +01:00
1481749c22 Use urlsplit instead of urlparse.
Closes #118.
2013-01-03 14:12:27 +01:00
d84d94dd55 Clean up 2013-01-03 13:49:41 +01:00
1913b0d438 Merge branch 'master' of github.com:jkbr/httpie 2012-12-19 12:31:34 +01:00
fe16f425a9 Require Requests v1.0.3. 2012-12-19 12:31:01 +01:00
7ff71a7f10 Revert: Test Python 3.3 on Travis.
3.3 still not supported
2012-12-19 11:56:02 +01:00
4a37d10245 Test Python 3.3 on Travis. 2012-12-19 11:53:26 +01:00
e5edb66ae8 Requests v1.0: Fixed request body access. 2012-12-19 11:37:52 +01:00
1766dd8291 Requests 1.0: session cookies. 2012-12-17 17:18:18 +01:00
675a8b17ad Merge branch 'master' of github.com:jkbr/httpie 2012-12-17 17:14:24 +01:00
69e26b8bc8 Requests 1.0: prefetch; default_headers. 2012-12-17 17:02:27 +01:00
291f520e0c Update README.rst 2012-12-17 12:26:57 +01:00
9ec328ff6f Session commands. 2012-12-11 12:54:34 +01:00
f2d59ba6bd Improved --check-status + HTTP error + stdout redirect warning. 2012-12-05 05:27:11 +01:00
53caf6ae72 Cleanup 2012-12-05 05:06:06 +01:00
8175366f27 PEP8 2012-12-05 04:39:56 +01:00
8190a7c0c6 Fixed httpie session list 2012-12-05 04:36:42 +01:00
4a615e762f Updated session docs. 2012-12-01 18:43:33 +01:00
7426b4b493 RST formatting. 2012-12-01 18:26:15 +01:00
2cdcadd9d5 Added docs for httpie. 2012-12-01 18:25:34 +01:00
18510a9396 Progress on httpie session *. 2012-12-01 18:16:00 +01:00
acf5f063c7 Typo 2012-12-01 16:52:23 +01:00
2cf379df78 Fixed README typo. 2012-12-01 16:20:16 +01:00
dd100c2cc4 Fixed -j & -v & redirected stdout. Closes #109. 2012-12-01 15:55:58 +01:00
444a9fa929 Added httpless to README. 2012-12-01 15:54:36 +01:00
4a24cd25b9 Clean up. 2012-12-01 15:20:14 +01:00
1c5fb89001 Output stream refactoring. 2012-11-09 15:49:23 +01:00
466e1dbedf Updated CHANGELOG (#100). 2012-11-08 22:39:28 +01:00
d87b2aa0e5 Added support for credentials in URL.
Closes #100 🍰
2012-11-08 22:29:54 +01:00
5d969852c7 Added --no-option's and made args more config-friendly. 2012-09-24 06:49:12 +02:00
bbc702fa11 Improved README. 2012-09-24 05:59:52 +02:00
e25d64a610 0.3.0 2012-09-21 05:50:01 +02:00
a41dd7ac6d Allow output redirection on Windows.
Closes #88.
2012-09-21 04:30:59 +02:00
4a6f32a0f4 Documented config.
Also renamed `default_content_type` to `implicit_content_type` .
2012-09-17 03:08:45 +02:00
548bef7dff Added tests for sessions. 2012-09-17 02:15:00 +02:00
6c2001d1f5 Use the HTTPIE_CONFIG_DIR environment variable. 2012-09-17 02:12:16 +02:00
4029dbf309 Added configuration file.
The "default_content_type" option can be set to "form".

Closes #91.
2012-09-17 00:37:36 +02:00
478d654945 Renamed --session-read to --session-read-only. 2012-09-17 00:01:49 +02:00
66bdbc3745 Cleanup. 2012-09-07 12:48:59 +02:00
316e3f45a9 Added --session-read for read-only sessions. 2012-09-07 12:38:52 +02:00
da0eb7db79 Renamed --allow-redirects to --follow. 2012-09-07 11:58:39 +02:00
9338aadd75 Cleanup 2012-09-05 20:22:08 +02:00
dc7d03e6b8 Merge pull request #90 from simonbuchan/898408c20cfab130699cee3bedbae1ad4a1c69b1
Fix --session for Windows (with a Requests patch)
2012-09-04 02:38:34 -07:00
898408c20c Fix sessions for Windows
':' is invalid in a Windows path, and json needs output to support
write(str).
2012-09-04 15:53:27 +12:00
47de4e2c9c Sessions are now host-bound. 2012-08-19 04:58:14 +02:00
f74424ef03 README 2012-08-18 23:11:56 +02:00
8a9cedb16e JSON session data, `httpie' management command. 2012-08-18 23:07:36 +02:00
ff9f23da5b Grouped arguments for a more user-friendly --help. 2012-08-18 06:12:44 +02:00
50810e5bd9 Include data directory location with --debug. 2012-08-18 04:45:29 +02:00
9b586b953b Use %APPDATA% for data on Windows. 2012-08-18 04:36:58 +02:00
149cbc1604 Fixed Solarized style unavailable on Windows.
#87.
2012-08-18 03:54:38 +02:00
d3df59c8af Updated README. 2012-08-17 23:35:36 +02:00
2057e13a1d Updated README. 2012-08-17 23:35:06 +02:00
4957686bcd Updated README. 2012-08-17 23:34:42 +02:00
4c0d7d526f Added initial support for persistent sessions. 2012-08-17 23:30:47 +02:00
0b3bad9c81 Added initial support for persistent sessions. 2012-08-17 23:23:02 +02:00
1ed43c1a1e Semver-compatible versioning. 2012-08-17 21:24:34 +02:00
bf03937f06 Unified output processing options under --pretty.
* --pretty=none instead of --ugly
* --pretty=all instead of --pretty
* --pretty=colors instead of --colors
* --pretty=format instead of --format
2012-08-17 21:15:37 +02:00
4660da949f Fixed colorized output on Windows with Python 3.
Closes #87.
2012-08-17 06:35:18 +02:00
86256af1df Removed non-ASCII characters from README (closes #85). 2012-08-16 18:47:30 +02:00
8bf7f8219c Fixed readme decoding.
Closes #85.
2012-08-16 03:11:15 +02:00
a5522b8233 Revert "Iter body lines to avoid binary false positives."
This reverts commit b92a3a6d95.
2012-08-16 03:06:48 +02:00
b92a3a6d95 Iter body lines to avoid binary false positives.
#84
2012-08-13 23:33:25 +02:00
9098e5b6e8 Updated changelog. 2012-08-12 06:02:13 +02:00
68640a81b3 Use CRLF for headers in the output. 2012-08-10 01:45:07 +02:00
27f08920c4 Improved examples. 2012-08-09 23:36:29 +02:00
c01dd8d64a Added exit status for timed-out requests. 2012-08-09 05:24:58 +02:00
76feea2f68 Added README reStructuredText validation. 2012-08-07 17:20:50 +02:00
22a10aec4a Added --colors and --format.
Closes #59 and #82.
2012-08-07 16:59:49 +02:00
fa334bdf4d Documented --verify. 2012-08-07 15:25:24 +02:00
f6724452cf Skip tests with redirects on Requests 0.13.6. 2012-08-07 15:08:28 +02:00
07de32c406 Version fix. 2012-08-07 15:01:04 +02:00
1fbe7a6121 Improved --debug. 2012-08-07 14:50:51 +02:00
49e44d9b7e Pre-process README.rst so that PyPi renders it. 2012-08-07 14:50:17 +02:00
193683afbb Added proxy docs. 2012-08-07 14:49:43 +02:00
126b1da515 v0.2.8dev 2012-08-07 00:13:27 +02:00
969b310ea9 v0.2.7 2012-08-07 00:12:47 +02:00
dd2c89412c Compatibility with Requests 0.13.6. 2012-08-07 00:07:04 +02:00
381e60f9d8 Extended README. 2012-08-06 23:27:49 +02:00
44e409693b Set JSON Content-Type only with data even with -j. 2012-08-06 22:14:52 +02:00
4e58a3849a Added exit status constants, cleaned up main(). 2012-08-04 19:22:50 +02:00
94c77c9bfc Improved password prompt. 2012-08-04 17:04:36 +02:00
747b87c4e6 Changelog, typos 2012-08-04 16:46:39 +02:00
c7657e3c4b Streamed terminal output
`--stream` can be used to enable streaming also with `--pretty` and to ensure
a more frequent output flushing.
2012-08-04 16:35:31 +02:00
4615011f2e Sort headers by name when prettifying. 2012-08-03 00:58:01 +02:00
4b1a04e5ed Fixed error handling. 2012-08-02 04:33:43 +02:00
e045ca6bd8 Cleanup, CHANGELOG 2012-08-01 23:51:30 +02:00
52e46bedda Take advantage of streaming.
It's now possible to download huge files with HTTPie, and it's often faster than curl and wget!
2012-08-01 23:21:52 +02:00
67ad5980b2 Don't fetch the response body unless needed.
E.g., this will only read the response headers but won't download the
whole file:

    http GET --headers example.org/big-file.avi

The request method is respected (i.e., it doesn't switch to HEAD like
cURL does).
2012-08-01 21:31:06 +02:00
00d85a4b97 Fallback to media subtype if the type is uknown.
Closes #81.
2012-08-01 17:37:23 +02:00
90d34ffd0d Added tests for binary request data. 2012-08-01 00:52:30 +02:00
8905b4fc72 cleanup 2012-07-30 14:23:22 +02:00
a5b98818c8 Syntax-highlighting for examples in the README. 2012-07-30 13:58:13 +02:00
5e7bb1f6dc Syntax-highlighting for examples in the README. 2012-07-30 13:51:28 +02:00
4117d99dd0 Updated screenshot. 2012-07-30 12:37:59 +02:00
49604e7c29 Updated screenshot. 2012-07-30 12:29:56 +02:00
72d371c467 Updated screenshot. 2012-07-30 12:24:11 +02:00
a8c9441f71 Updated screenshot. 2012-07-30 12:11:28 +02:00
e13f65ace1 Updated solarized and switched to Solarized256Style. 2012-07-30 12:11:16 +02:00
a1682d0d2e Added AUTHORS 2012-07-30 12:10:19 +02:00
923a8b71bd Revorked output
Binary now works everywhere. Also added `--output FILE` for Windows.
2012-07-30 10:58:16 +02:00
6eed0d92eb Better error messages. 2012-07-29 07:14:54 +02:00
edf87c3392 Consistent request-response separators. 2012-07-29 06:59:51 +02:00
f73bfea6b8 Validate "file fields (name@/path) require --form / -f". 2012-07-29 06:58:50 +02:00
16635870e3 Removed redundant decode/encode. 2012-07-29 03:52:24 +02:00
f5bc081fda Send filenames with multipart/form-data file uploads. 2012-07-28 13:24:44 +02:00
1efea59a8d Fixed typos. 2012-07-28 06:09:25 +02:00
098e1d3100 Fixed multipart requests output; binary support.
* Bodies of multipart requests are correctly printed (closes #30).
* Binary requests and responses should always work (they are also suppressed
  for terminal output). So things like this work::

     http www.google.com/favicon.ico > favicon.ico
2012-07-28 05:50:12 +02:00
a8ddb8301d Default to https:// if invoked as `https'. 2012-07-27 18:08:33 +02:00
a770d79aef v0.2.7dev 2012-07-26 10:03:34 +02:00
23 changed files with 3282 additions and 1000 deletions

5
.gitignore vendored
View File

@ -3,3 +3,8 @@ httpie.egg-info
build build
*.pyc *.pyc
.tox .tox
README.html
.coverage
htmlcov
.idea
.DS_Store

View File

@ -3,7 +3,7 @@ python:
- 2.6 - 2.6
- 2.7 - 2.7
- pypy - pypy
- 3.2 - 3.3
script: python setup.py test script: python setup.py test
install: install:
- pip install . --use-mirrors - pip install . --use-mirrors

30
AUTHORS.rst Normal file
View File

@ -0,0 +1,30 @@
==============
HTTPie authors
==============
* `Jakub Roztocil <https://github.com/jkbr>`_
Patches and ideas
-----------------
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
* `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_
* `Vladimir Berkutov <https://github.com/dair-targ>`_
* `Jakob Kramer <https://github.com/gandaro>`_
* `Chris Faulkner <https://github.com/faulkner>`_
* `Alen Mujezinovic <https://github.com/flashingpumpkin>`_
* `Praful Mathur <https://github.com/tictactix>`_
* `Marc Abramowitz <https://github.com/msabramo>`_
* `Ismail Badawi <https://github.com/isbadawi>`_
* `Laurent Bachelier <https://github.com/laurentb>`_
* `Isman Firmansyah <https://github.com/iromli>`_
* `Simon Olofsson <https://github.com/simono>`_
* `Churkin Oleg <https://github.com/Bahus>`_
* `Jökull Sólberg Auðunsson <https://github.com/jokull>`_
* `Matthew M. Boedicker <https://github.com/mmb>`_
* `marblar <https://github.com/marblar>`_
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
* `Davey Shafik <https://github.com/dshafik>`_
* `cido <https://github.com/cido>`_

1261
README.rst

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 446 KiB

View File

@ -1,7 +1,19 @@
""" """
HTTPie - cURL for humans. HTTPie - a CLI, cURL-like tool for humans.
""" """
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.2.6' __version__ = '0.4.0'
__licence__ = 'BSD' __licence__ = 'BSD'
class ExitStatus:
"""Exit status code constants."""
OK = 0
ERROR = 1
ERROR_TIMEOUT = 2
# Used only when requested with --check-status:
ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5

View File

@ -1,16 +1,22 @@
"""CLI arguments definition. """CLI arguments definition.
NOTE: the CLI interface may change before reaching v1.0. NOTE: the CLI interface may change before reaching v1.0.
TODO: make the options config friendly, i.e., no mutually exclusive groups to
allow options overwriting.
""" """
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
from . import __doc__ from . import __doc__
from . import __version__ from . import __version__
from .output import AVAILABLE_STYLES from .compat import is_windows
from .sessions import DEFAULT_SESSIONS_DIR, Session
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
PRETTIFY_STDOUT_TTY_ONLY,
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)
def _(text): def _(text):
@ -18,15 +24,72 @@ def _(text):
return ' '.join(text.strip().split()) return ' '.join(text.strip().split())
parser = Parser(description='%s <http://httpie.org>' % __doc__.strip()) parser = Parser(
parser.add_argument('--version', action='version', version=__version__) description='%s <http://httpie.org>' % __doc__.strip(),
epilog='For every --option there is a --no-option'
' that reverts the option to its default value.\n\n'
'Suggestions and bug reports are greatly appreciated:\n'
'https://github.com/jkbr/httpie/issues'
)
###############################################################################
# Positional arguments.
###############################################################################
positional = parser.add_argument_group(
title='Positional arguments',
description=_('''
These arguments come after any flags and in the
order they are listed here. Only URL is required.
''')
)
positional.add_argument(
'method', metavar='METHOD',
nargs=OPTIONAL,
default=None,
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted, then HTTPie
will guess the HTTP method. If there is some
data to be sent, then it will be POST, otherwise GET.
''')
)
positional.add_argument(
'url', metavar='URL',
help=_('''
The protocol defaults to http:// if the
URL does not include one.
''')
)
positional.add_argument(
'items', metavar='REQUEST ITEM',
nargs=ZERO_OR_MORE,
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=_('''
A key-value pair whose type is defined by the
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.
''')
)
###############################################################################
# Content type. # Content type.
############################################# ###############################################################################
group_type = parser.add_mutually_exclusive_group(required=False) content_type = parser.add_argument_group(
group_type.add_argument( title='Predefined content types',
description=None
)
content_type.add_argument(
'--json', '-j', action='store_true', '--json', '-j', action='store_true',
help=_(''' help=_('''
(default) Data items from the command (default) Data items from the command
@ -35,47 +98,66 @@ group_type.add_argument(
are set to application/json (if not specified). are set to application/json (if not specified).
''') ''')
) )
group_type.add_argument( content_type.add_argument(
'--form', '-f', action='store_true', '--form', '-f', action='store_true',
help=_(''' help=_('''
Data items from the command line are serialized as form fields. Data items from the command line are serialized as form fields.
The Content-Type is set to application/x-www-form-urlencoded The Content-Type is set to application/x-www-form-urlencoded
(if not specified). (if not specified).
The presence of any file fields results The presence of any file fields results
into a multipart/form-data request. in a multipart/form-data request.
''') ''')
) )
# Output options. ###############################################################################
############################################# # Output processing
###############################################################################
parser.add_argument( output_processing = parser.add_argument_group(title='Output processing')
'--traceback', action='store_true', default=False,
output_processing.add_argument(
'--output', '-o', type=FileType('w+b'),
metavar='FILE',
help=SUPPRESS if not is_windows else _(
'''
Save output to FILE.
This option is a replacement for piping output to FILE,
which would on Windows result in corrupted data
being saved.
'''
)
)
output_processing.add_argument(
'--pretty', dest='prettify', default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()),
help=_(''' help=_('''
Print exception traceback should one occur. Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors
and formatting
(default for terminal output), "colors", or "format".
''') ''')
) )
output_processing.add_argument(
'--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
choices=AVAILABLE_STYLES,
help=_('''
Output coloring style. One of %s. Defaults to "%s".
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
)
prettify = parser.add_mutually_exclusive_group(required=False)
prettify.add_argument(
'--pretty', dest='prettify', action='store_true',
default=PRETTIFY_STDOUT_TTY_ONLY,
help=_('''
If stdout is a terminal, the response is prettified
by default (colorized and indented if it is JSON).
This flag ensures prettifying even when stdout is redirected.
''')
)
prettify.add_argument(
'--ugly', '-u', dest='prettify', action='store_false',
help=_('''
Do not prettify the response.
''')
)
output_options = parser.add_mutually_exclusive_group(required=False) ###############################################################################
output_options.add_argument('--print', '-p', dest='output_options', # Output options
###############################################################################
output_options = parser.add_argument_group(title='Output options')
output_options.add_argument(
'--print', '-p', dest='output_options', metavar='WHAT',
help=_(''' help=_('''
String specifying what the output should contain: String specifying what the output should contain:
"{request_headers}" stands for the request headers, and "{request_headers}" stands for the request headers, and
@ -86,12 +168,10 @@ output_options.add_argument('--print', '-p', dest='output_options',
headers and body is printed), if standard output is not redirected. headers and body is printed), if standard output is not redirected.
If the output is piped to another program or to a file, If the output is piped to another program or to a file,
then only the body is printed by default. then only the body is printed by default.
'''.format( '''.format(request_headers=OUT_REQ_HEAD,
request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY, request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD, response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY, response_body=OUT_RESP_BODY,))
))
) )
output_options.add_argument( output_options.add_argument(
'--verbose', '-v', dest='output_options', '--verbose', '-v', dest='output_options',
@ -118,18 +198,114 @@ output_options.add_argument(
'''.format(OUT_RESP_BODY)) '''.format(OUT_RESP_BODY))
) )
parser.add_argument( output_options.add_argument(
'--style', '-s', dest='style', default='solarized', metavar='STYLE', '--stream', '-S', action='store_true', default=False,
choices=AVAILABLE_STYLES,
help=_(''' help=_('''
Output coloring style, one of %s. Defaults to solarized. Always stream the output by line, i.e., behave like `tail -f'.
For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar Without --stream and with --pretty (either set or implied),
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc). HTTPie fetches the whole response before it outputs the processed data.
''') % ', '.join(sorted(AVAILABLE_STYLES))
Set this option when you want to continuously display a prettified
long-lived response, such as one from the Twitter streaming API.
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
''')
) )
parser.add_argument(
###############################################################################
# Sessions
###############################################################################
sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False)
sessions.add_argument(
'--session', metavar='SESSION_NAME', type=RegexValidator(
Session.VALID_NAME_PATTERN,
'Session name contains invalid characters.'
),
help=_('''
Create, or reuse and update a session.
Within a session, custom headers, auth credential, as well as any
cookies sent by the server persist between requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR)
)
sessions.add_argument(
'--session-read-only', metavar='SESSION_NAME',
help=_('''
Create or read a session without updating it form the
request/response exchange.
''')
)
###############################################################################
# Authentication
###############################################################################
# ``requests.request`` keyword arguments.
auth = parser.add_argument_group(title='Authentication')
auth.add_argument(
'--auth', '-a', metavar='USER[:PASS]',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_('''
If only the username is provided (-a username),
HTTPie will prompt for the password.
'''),
)
auth.add_argument(
'--auth-type', choices=['basic', 'digest'], default='basic',
help=_('''
The authentication mechanism to be used.
Defaults to "basic".
''')
)
# Network
#############################################
network = parser.add_argument_group(title='Network')
network.add_argument(
'--proxy', default=[], action='append', metavar='PROTOCOL:HOST',
type=KeyValueArgType(SEP_PROXY),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128). You can specify multiple
proxies with different protocols.
''')
)
network.add_argument(
'--follow', default=False, action='store_true',
help=_('''
Set this flag if full redirects are allowed
(e.g. re-POST-ing of data at new ``Location``)
''')
)
network.add_argument(
'--verify', default='yes',
help=_('''
Set to "no" to skip checking the host\'s SSL certificate.
You can also pass the path to a CA_BUNDLE
file for private certs. You can also set
the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes".
''')
)
network.add_argument(
'--timeout', type=float, default=30, metavar='SECONDS',
help=_('''
The connection timeout of the request in seconds.
The default value is 30 seconds.
''')
)
network.add_argument(
'--check-status', default=False, action='store_true', '--check-status', default=False, action='store_true',
help=_(''' help=_('''
By default, HTTPie exits with 0 when no network or other fatal By default, HTTPie exits with 0 when no network or other fatal
@ -140,7 +316,7 @@ parser.add_argument(
When the server replies with a 4xx (Client Error) or 5xx When the server replies with a 4xx (Client Error) or 5xx
(Server Error) status code, HTTPie exits with 4 or 5 respectively. (Server Error) status code, HTTPie exits with 4 or 5 respectively.
If the response is a 3xx (Redirect) and --allow-redirects If the response is a 3xx (Redirect) and --follow
hasn't been set, then the exit status is 3. hasn't been set, then the exit status is 3.
Also an error message is written to stderr if stdout is redirected. Also an error message is written to stderr if stdout is redirected.
@ -148,93 +324,29 @@ parser.add_argument(
''') ''')
) )
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a',
type=AuthCredentialsArgType(SEP_CREDENTIALS),
help=_('''
username:password.
If only the username is provided (-a username),
HTTPie will prompt for the password.
'''),
)
parser.add_argument( ###############################################################################
'--auth-type', choices=['basic', 'digest'], default='basic', # Troubleshooting
help=_(''' ###############################################################################
The authentication mechanism to be used.
Defaults to "basic".
''')
)
parser.add_argument( troubleshooting = parser.add_argument_group(title='Troubleshooting')
'--verify', default='yes',
help=_('''
Set to "no" to skip checking the host\'s SSL certificate.
You can also pass the path to a CA_BUNDLE
file for private certs. You can also set
the REQUESTS_CA_BUNDLE environment variable.
Defaults to "yes".
''')
)
parser.add_argument(
'--proxy', default=[], action='append',
type=KeyValueArgType(SEP_PROXY),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
''')
)
parser.add_argument(
'--allow-redirects', default=False, action='store_true',
help=_('''
Set this flag if full redirects are allowed
(e.g. re-POST-ing of data at new ``Location``)
''')
)
parser.add_argument(
'--timeout', type=float,
help=_('''
Float describes the timeout of the request
(Use socket.setdefaulttimeout() as fallback).
''')
)
troubleshooting.add_argument(
# Positional arguments. '--help',
############################################# action='help', default=SUPPRESS,
help='Show this help message and exit'
parser.add_argument( )
'method', metavar='METHOD', troubleshooting.add_argument(
nargs='?', '--version', action='version', version=__version__)
default=None, troubleshooting.add_argument(
'--traceback', action='store_true', default=False,
help='Prints exception traceback should one occur.'
)
troubleshooting.add_argument(
'--debug', action='store_true', default=False,
help=_(''' help=_('''
The HTTP method to be used for the request Prints exception traceback should one occur, and also other
(GET, POST, PUT, DELETE, PATCH, ...). information that is useful for debugging HTTPie itself and
If this argument is omitted, then HTTPie for bug reports.
will guess the HTTP method. If there is some
data to be sent, then it will be POST, otherwise GET.
''')
)
parser.add_argument(
'url', metavar='URL',
help=_('''
The protocol defaults to http:// if the
URL does not include one.
''')
)
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
type=KeyValueArgType(*SEP_GROUP_ITEMS),
help=_('''
A key-value pair whose type is defined by the
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.
''') ''')
) )

94
httpie/client.py Normal file
View File

@ -0,0 +1,94 @@
import json
import sys
from pprint import pformat
import requests
import requests.auth
from . import sessions
from . import __version__
FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; charset=utf-8'
DEFAULT_UA = 'HTTPie/%s' % __version__
def get_response(args, config_dir):
"""Send the request and return a `request.Response`."""
requests_kwargs = get_requests_kwargs(args)
if args.debug:
sys.stderr.write('\n>>> requests.request(%s)\n\n'
% pformat(requests_kwargs))
if not args.session and not args.session_read_only:
response = requests.request(**requests_kwargs)
else:
response = sessions.get_response(
config_dir=config_dir,
name=args.session or args.session_read_only,
request_kwargs=requests_kwargs,
read_only=bool(args.session_read_only),
)
return response
def get_requests_kwargs(args):
"""Translate our `args` into `requests.request` keyword arguments."""
implicit_headers = {
'User-Agent': DEFAULT_UA
}
auto_json = args.data and not args.form
if args.json or auto_json:
implicit_headers['Accept'] = 'application/json'
if args.data:
implicit_headers['Content-Type'] = JSON
if isinstance(args.data, dict):
if args.data:
args.data = json.dumps(args.data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
args.data = ''
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
implicit_headers['Content-Type'] = FORM
for name, value in implicit_headers.items():
if name not in args.headers:
args.headers[name] = value
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'stream': True,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
'data': args.data,
'verify': {
'yes': True,
'no': False
}.get(args.verify, args.verify),
'timeout': args.timeout,
'auth': credentials,
'proxies': dict((p.key, p.value) for p in args.proxy),
'files': args.files,
'allow_redirects': args.follow,
'params': args.params,
}
return kwargs

18
httpie/compat.py Normal file
View File

@ -0,0 +1,18 @@
"""
Python 2/3 compatibility.
"""
#noinspection PyUnresolvedReferences
from requests.compat import (
is_windows,
bytes,
str,
is_py3,
is_py26,
)
try:
from urllib.parse import urlsplit
except ImportError:
from urlparse import urlsplit

95
httpie/config.py Normal file
View File

@ -0,0 +1,95 @@
import os
import json
import errno
from . import __version__
from .compat import is_windows
DEFAULT_CONFIG_DIR = os.environ.get(
'HTTPIE_CONFIG_DIR',
os.path.expanduser('~/.httpie') if not is_windows else
os.path.expandvars(r'%APPDATA%\\httpie')
)
class BaseConfigDict(dict):
name = None
help = None
about = None
directory = DEFAULT_CONFIG_DIR
def __init__(self, directory=None, *args, **kwargs):
super(BaseConfigDict, self).__init__(*args, **kwargs)
if directory:
self.directory = directory
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
return os.path.join(self.directory, self.name + '.json')
@property
def is_new(self):
return not os.path.exists(self.path)
def load(self):
try:
with open(self.path, 'rt') as f:
try:
data = json.load(f)
except ValueError as e:
raise ValueError(
'Invalid %s JSON: %s [%s]' %
(type(self).__name__, e.message, self.path)
)
self.update(data)
except IOError as e:
if e.errno != errno.ENOENT:
raise
def save(self):
self['__meta__'] = {
'httpie': __version__
}
if self.help:
self['__meta__']['help'] = self.help
if self.about:
self['__meta__']['about'] = self.about
with open(self.path, 'w') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write('\n')
def delete(self):
try:
os.unlink(self.path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
class Config(BaseConfigDict):
name = 'config'
help = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file'
DEFAULTS = {
'implicit_content_type': 'json',
'default_options': []
}
def __init__(self, *args, **kwargs):
super(Config, self).__init__(*args, **kwargs)
self.update(self.DEFAULTS)

View File

@ -3,169 +3,127 @@
Invocation flow: Invocation flow:
1. Read, validate and process the input (args, `stdin`). 1. Read, validate and process the input (args, `stdin`).
2. Create a request and send it, get the response. 2. Create and send a request.
3. Process and format the requested parts of the request-response exchange. 3. Stream, and possibly process and format, the requested parts
4. Write to `stdout` and exit. of the request-response exchange.
4. Simultaneously write to `stdout`
5. Exit.
""" """
import sys import sys
import json import errno
import requests import requests
import requests.auth from httpie import __version__ as httpie_version
from requests.compat import str from requests import __version__ as requests_version
from pygments import __version__ as pygments_version
from .models import HTTPMessage, Environment
from .output import OutputProcessor
from .input import (PRETTIFY_STDOUT_TTY_ONLY,
OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY)
from .cli import parser from .cli import parser
from .compat import str, is_py3
from .client import get_response
from .models import Environment
from .output import build_output_stream, write, write_with_colors_win_p3k
from . import ExitStatus
TYPE_FORM = 'application/x-www-form-urlencoded; charset=utf-8' def get_exit_status(http_status, follow=False):
TYPE_JSON = 'application/json; charset=utf-8' """Translate HTTP status code to exit status code."""
if 300 <= http_status <= 399 and not follow:
def get_response(args, env):
"""Send the request and return a `request.Response`."""
auto_json = args.data and not args.form
if args.json or auto_json:
if 'Content-Type' not in args.headers:
args.headers['Content-Type'] = TYPE_JSON
if 'Accept' not in args.headers:
# Default Accept to JSON as well.
args.headers['Accept'] = 'application/json'
if isinstance(args.data, dict):
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form:
if not args.files and 'Content-Type' not in args.headers:
# If sending files, `requests` will set
# the `Content-Type` for us.
args.headers['Content-Type'] = TYPE_FORM
try:
credentials = None
if args.auth:
credentials = {
'basic': requests.auth.HTTPBasicAuth,
'digest': requests.auth.HTTPDigestAuth,
}[args.auth_type](args.auth.key, args.auth.value)
return requests.request(
method=args.method.lower(),
url=args.url if '://' in args.url else 'http://%s' % args.url,
headers=args.headers,
data=args.data,
verify={'yes': True, 'no': False}.get(args.verify, args.verify),
timeout=args.timeout,
auth=credentials,
proxies=dict((p.key, p.value) for p in args.proxy),
files=args.files,
allow_redirects=args.allow_redirects,
params=args.params,
)
except (KeyboardInterrupt, SystemExit):
env.stderr.write('\n')
sys.exit(1)
except Exception as e:
if args.traceback:
raise
env.stderr.write(str(e.message) + '\n')
sys.exit(1)
def get_output(args, env, request, response):
"""Format parts of the `request`-`response` exchange
according to `args` and `env` and return a `unicode`.
"""
do_prettify = (args.prettify is True
or (args.prettify == PRETTIFY_STDOUT_TTY_ONLY
and env.stdout_isatty))
do_output_request = (OUT_REQ_HEAD in args.output_options
or OUT_REQ_BODY in args.output_options)
do_output_response = (OUT_RESP_HEAD in args.output_options
or OUT_RESP_BODY in args.output_options)
prettifier = None
if do_prettify:
prettifier = OutputProcessor(
env, pygments_style=args.style)
buf = []
if do_output_request:
req_msg = HTTPMessage.from_request(request)
req = req_msg.format(
prettifier=prettifier,
with_headers=OUT_REQ_HEAD in args.output_options,
with_body=OUT_REQ_BODY in args.output_options
)
buf.append(req)
buf.append('\n')
if do_output_response:
buf.append('\n')
if do_output_response:
resp_msg = HTTPMessage.from_response(response)
resp = resp_msg.format(
prettifier=prettifier,
with_headers=OUT_RESP_HEAD in args.output_options,
with_body=OUT_RESP_BODY in args.output_options
)
buf.append(resp)
buf.append('\n')
return ''.join(buf)
def get_exist_status(code, allow_redirects=False):
"""Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not allow_redirects:
# Redirect # Redirect
return 3 return ExitStatus.ERROR_HTTP_3XX
elif 400 <= code <= 499: elif 400 <= http_status <= 499:
# Client Error # Client Error
return 4 return ExitStatus.ERROR_HTTP_4XX
elif 500 <= code <= 599: elif 500 <= http_status <= 599:
# Server Error # Server Error
return 5 return ExitStatus.ERROR_HTTP_5XX
else: else:
return 0 return ExitStatus.OK
def print_debug_info(env):
sys.stderr.writelines([
'HTTPie %s\n' % httpie_version,
'HTTPie data: %s\n' % env.config.directory,
'Requests %s\n' % requests_version,
'Pygments %s\n' % pygments_version,
'Python %s %s\n' % (sys.version, sys.platform)
])
def main(args=sys.argv[1:], env=Environment()): def main(args=sys.argv[1:], env=Environment()):
"""Run the main program and write the output to ``env.stdout``. """Run the main program and write the output to ``env.stdout``.
Return exit status. Return exit status code.
""" """
args = parser.parse_args(args=args, env=env) if env.config.default_options:
response = get_response(args, env) args = env.config.default_options + args
status = 0 def error(msg, *args, **kwargs):
msg = msg % args
level = kwargs.get('level', 'error')
env.stderr.write('http: %s: %s\n' % (level, msg))
debug = '--debug' in args
traceback = debug or '--traceback' in args
exit_status = ExitStatus.OK
if debug:
print_debug_info(env)
if args == ['--debug']:
return exit_status
try:
args = parser.parse_args(args=args, env=env)
response = get_response(args, config_dir=env.config.directory)
if args.check_status: if args.check_status:
status = get_exist_status(response.status_code, exit_status = get_exit_status(response.status_code, args.follow)
args.allow_redirects)
if status and not env.stdout_isatty:
err = 'http error: %s %s\n' % (
response.raw.status, response.raw.reason)
env.stderr.write(err.encode('utf8'))
output = get_output(args, env, response.request, response) if not env.stdout_isatty and exit_status != ExitStatus.OK:
output_bytes = output.encode('utf8') error('HTTP %s %s',
f = getattr(env.stdout, 'buffer', env.stdout) response.raw.status,
f.write(output_bytes) response.raw.reason,
level='warning')
return status write_kwargs = {
'stream': build_output_stream(args, env,
response.request,
response),
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
try:
if env.is_windows and is_py3 and 'colors' in args.prettify:
write_with_colors_win_p3k(**write_kwargs)
else:
write(**write_kwargs)
except IOError as e:
if not traceback and e.errno == errno.EPIPE:
# Ignore broken pipes unless --traceback.
env.stderr.write('\n')
else:
raise
except (KeyboardInterrupt, SystemExit):
if traceback:
raise
env.stderr.write('\n')
exit_status = ExitStatus.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
error('Request timed out (%ss).', args.timeout)
except Exception as e:
# TODO: Better distinction between expected and unexpected errors.
# Network errors vs. bugs, etc.
if traceback:
raise
error('%s: %s', type(e).__name__, str(e))
exit_status = ExitStatus.ERROR
return exit_status

View File

@ -5,23 +5,27 @@ import os
import sys import sys
import re import re
import json import json
import argparse
import mimetypes import mimetypes
import getpass import getpass
from io import BytesIO
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
try: try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
OrderedDict = dict OrderedDict = dict
# TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jkbr/httpie/issues/130
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from requests.compat import str
from . import __version__ from .compat import urlsplit, str
HTTP_POST = 'POST' HTTP_POST = 'POST'
HTTP_GET = 'GET' HTTP_GET = 'GET'
HTTP = 'http://'
HTTPS = 'https://'
# Various separators used in args # Various separators used in args
@ -63,15 +67,22 @@ OUTPUT_OPTIONS = frozenset([
OUT_RESP_BODY OUT_RESP_BODY
]) ])
# Pretty
PRETTY_MAP = {
'all': ['format', 'colors'],
'colors': ['colors'],
'format': ['format'],
'none': []
}
PRETTY_STDOUT_TTY_ONLY = object()
# Defaults # Defaults
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class Parser(argparse.ArgumentParser): class Parser(ArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`. """Adds additional logic to `argparse.ArgumentParser`.
Handles all input (CLI args, file args, stdin), applies defaults, Handles all input (CLI args, file args, stdin), applies defaults,
@ -82,39 +93,107 @@ class Parser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['add_help'] = False kwargs['add_help'] = False
super(Parser, self).__init__(*args, **kwargs) super(Parser, self).__init__(*args, **kwargs)
# Help only as --help (-h is used for --headers).
self.add_argument('--help',
action='help', default=argparse.SUPPRESS,
help=argparse._('show this help message and exit'))
#noinspection PyMethodOverriding #noinspection PyMethodOverriding
def parse_args(self, env, args=None, namespace=None): def parse_args(self, env, args=None, namespace=None):
args = super(Parser, self).parse_args(args, namespace) self.env = env
args, no_options = super(Parser, self).parse_known_args(args,
namespace)
self._apply_no_options(args, no_options)
if not args.json and env.config.implicit_content_type == 'form':
args.form = True
if args.debug:
args.traceback = True
if args.output:
env.stdout = args.output
env.stdout_isatty = False
self._process_output_options(args, env) self._process_output_options(args, env)
self._process_pretty_options(args, env)
self._guess_method(args, env) self._guess_method(args, env)
self._parse_items(args) self._parse_items(args)
if not env.stdin_isatty: if not env.stdin_isatty:
self._body_from_file(args, env.stdin) self._body_from_file(args, env.stdin)
if args.auth and not args.auth.has_password(): if not (args.url.startswith(HTTP) or args.url.startswith(HTTPS)):
# Stdin already read (if not a tty) so it's save to prompt. scheme = HTTPS if env.progname == 'https' else HTTP
args.auth.prompt_password() args.url = scheme + args.url
self._process_auth(args)
return args return args
def _body_from_file(self, args, f): def _process_auth(self, args):
"""Use the content of `f` as the `request.data`. url = urlsplit(args.url)
There can only be one source of request data. if args.auth:
if not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(url.netloc)
elif url.username is not None:
# Handle http://username:password@hostname/
username, password = url.username, url.password
args.auth = AuthCredentials(
key=username,
value=password,
sep=SEP_CREDENTIALS,
orig=SEP_CREDENTIALS.join([username, password])
)
def _apply_no_options(self, args, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g.,
specified in config.
"""
invalid = []
for option in no_options:
if not option.startswith('--no-'):
invalid.append(option)
continue
# --no-option => --option
inverted = '--' + option[5:]
for action in self._actions:
if inverted in action.option_strings:
setattr(args, action.dest, action.default)
break
else:
invalid.append(option)
if invalid:
msg = 'unrecognized arguments: %s'
self.error(msg % ' '.join(invalid))
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
sys.stdout: self.env.stdout,
sys.stderr: self.env.stderr,
None: self.env.stderr
}.get(file, file)
super(Parser, self)._print_message(message, file)
def _body_from_file(self, args, fd):
"""There can only be one source of request data.
Bytes are always read.
""" """
if args.data: if args.data:
self.error('Request body (from stdin or a file) and request ' self.error('Request body (from stdin or a file) and request '
'data (key=value) cannot be mixed.') 'data (key=value) cannot be mixed.')
args.data = f.read() args.data = getattr(fd, 'buffer', fd).read()
def _guess_method(self, args, env): def _guess_method(self, args, env):
"""Set `args.method` if not specified to either POST or GET """Set `args.method` if not specified to either POST or GET
@ -138,7 +217,7 @@ class Parser(argparse.ArgumentParser):
args.items.insert( args.items.insert(
0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url)) 0, KeyValueArgType(*SEP_GROUP_ITEMS).__call__(args.url))
except argparse.ArgumentTypeError as e: except ArgumentTypeError as e:
if args.traceback: if args.traceback:
raise raise
self.error(e.message) self.error(e.message)
@ -157,7 +236,6 @@ class Parser(argparse.ArgumentParser):
""" """
args.headers = CaseInsensitiveDict() args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = ParamDict() if args.form else OrderedDict() args.data = ParamDict() if args.form else OrderedDict()
args.files = OrderedDict() args.files = OrderedDict()
args.params = ParamDict() args.params = ParamDict()
@ -175,23 +253,17 @@ class Parser(argparse.ArgumentParser):
if args.files and not args.form: if args.files and not args.form:
# `http url @/path/to/file` # `http url @/path/to/file`
# It's not --form so the file contents will be used as the file_fields = list(args.files.keys())
# body of the requests. Also, we try to detect the appropriate if file_fields != ['']:
# Content-Type.
if len(args.files) > 1:
self.error( self.error(
'Only one file can be specified unless' 'Invalid file fields (perhaps you meant --form?): %s'
' --form is used. File fields: %s' % ','.join(file_fields))
% ','.join(args.files.keys()))
f = list(args.files.values())[0] fn, fd = args.files['']
self._body_from_file(args, f)
# Reset files
args.files = {} args.files = {}
self._body_from_file(args, fd)
if 'Content-Type' not in args.headers: if 'Content-Type' not in args.headers:
mime, encoding = mimetypes.guess_type(f.name, strict=False) mime, encoding = mimetypes.guess_type(fn, strict=False)
if mime: if mime:
content_type = mime content_type = mime
if encoding: if encoding:
@ -212,6 +284,14 @@ class Parser(argparse.ArgumentParser):
if unknown: if unknown:
self.error('Unknown output options: %s' % ','.join(unknown)) self.error('Unknown output options: %s' % ','.join(unknown))
def _process_pretty_options(self, args, env):
if args.prettify == PRETTY_STDOUT_TTY_ONLY:
args.prettify = PRETTY_MAP['all' if env.stdout_isatty else 'none']
elif args.prettify and env.is_windows:
self.error('Only terminal output can be colorized on Windows.')
else:
args.prettify = PRETTY_MAP[args.prettify]
class ParseError(Exception): class ParseError(Exception):
pass pass
@ -230,6 +310,38 @@ class KeyValue(object):
return self.__dict__ == other.__dict__ 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): class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`. """A key-value pair argument type used with `argparse`.
@ -310,7 +422,7 @@ class KeyValueArgType(object):
break break
else: else:
raise argparse.ArgumentTypeError( raise ArgumentTypeError(
'"%s" is not a valid value' % string) '"%s" is not a valid value' % string)
return self.key_value_class( return self.key_value_class(
@ -327,9 +439,10 @@ class AuthCredentials(KeyValue):
def has_password(self): def has_password(self):
return self.value is not None return self.value is not None
def prompt_password(self): def prompt_password(self, host):
try: try:
self.value = self._getpass("Password for user '%s': " % self.key) self.value = self._getpass(
'http: password for %s@%s: ' % (self.key, host))
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
sys.stderr.write('\n') sys.stderr.write('\n')
sys.exit(0) sys.exit(0)
@ -348,7 +461,7 @@ class AuthCredentialsArgType(KeyValueArgType):
""" """
try: try:
return super(AuthCredentialsArgType, self).__call__(string) return super(AuthCredentialsArgType, self).__call__(string)
except argparse.ArgumentTypeError: except ArgumentTypeError:
# No password provided, will prompt for it later. # No password provided, will prompt for it later.
return self.key_value_class( return self.key_value_class(
key=string, key=string,
@ -370,9 +483,6 @@ class ParamDict(OrderedDict):
data and URL params. data and URL params.
""" """
# NOTE: Won't work when used for form data with multiple values
# for a field and a file field is present:
# https://github.com/kennethreitz/requests/issues/737
if key not in self: if key not in self:
super(ParamDict, self).__setitem__(key, value) super(ParamDict, self).__setitem__(key, value)
else: else:
@ -406,12 +516,12 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
target = params target = params
elif item.sep == SEP_FILES: elif item.sep == SEP_FILES:
try: try:
value = open(os.path.expanduser(item.value), 'r') with open(os.path.expanduser(value), 'rb') as f:
value = (os.path.basename(value),
BytesIO(f.read()))
except IOError as e: except IOError as e:
raise ParseError( raise ParseError(
'Invalid argument "%s": %s' % (item.orig, e)) 'Invalid argument "%s": %s' % (item.orig, e))
if not key:
key = os.path.basename(value.name)
target = files target = files
elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]: elif item.sep in [SEP_DATA, SEP_DATA_RAW_JSON]:

View File

@ -1,6 +1,8 @@
import os import os
import sys import sys
from requests.compat import urlparse, is_windows
from .config import DEFAULT_CONFIG_DIR, Config
from .compat import urlsplit, is_windows, bytes, str
class Environment(object): class Environment(object):
@ -10,130 +12,164 @@ class Environment(object):
and allows for mocking. and allows for mocking.
""" """
is_windows = is_windows
progname = os.path.basename(sys.argv[0])
if progname not in ['http', 'https']:
progname = 'http'
stdin_isatty = sys.stdin.isatty() stdin_isatty = sys.stdin.isatty()
stdin = sys.stdin stdin = sys.stdin
if is_windows:
# `colorama` patches `sys.stdout` so its initialization
# needs to happen before the default environment is set.
import colorama
colorama.init()
del colorama
stdout_isatty = sys.stdout.isatty() stdout_isatty = sys.stdout.isatty()
stdout = sys.stdout
config_dir = DEFAULT_CONFIG_DIR
if stdout_isatty and is_windows:
from colorama.initialise import wrap_stream
stdout = wrap_stream(sys.stdout, convert=None,
strip=None, autoreset=True, wrap=True)
else:
stdout = sys.stdout
stderr = sys.stderr stderr = sys.stderr
# Can be set to 0 to disable colors completely. # Can be set to 0 to disable colors completely.
colors = 256 if '256color' in os.environ.get('TERM', '') else 88 colors = 256 if '256color' in os.environ.get('TERM', '') else 88
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert all(hasattr(type(self), attr)
for attr in kwargs.keys())
self.__dict__.update(**kwargs) self.__dict__.update(**kwargs)
@property
def config(self):
if not hasattr(self, '_config'):
self._config = Config(directory=self.config_dir)
if self._config.is_new:
self._config.save()
else:
self._config.load()
return self._config
class HTTPMessage(object): class HTTPMessage(object):
"""Model representing an HTTP message.""" """Abstract class for HTTP messages."""
def __init__(self, line, headers, body, content_type=None): def __init__(self, orig):
# {Request,Status}-Line self._orig = orig
self.line = line
self.headers = headers
self.body = body
self.content_type = content_type
def format(self, prettifier=None, with_headers=True, with_body=True): def iter_body(self, chunk_size):
"""Return a `unicode` representation of `self`. """ """Return an iterator over the body."""
pretty = prettifier is not None raise NotImplementedError()
bits = []
if with_headers: def iter_lines(self, chunk_size):
bits.append(self.line) """Return an iterator over the body yielding (`line`, `line_feed`)."""
bits.append(self.headers) raise NotImplementedError()
if pretty:
bits = [
prettifier.process_headers('\n'.join(bits))
]
if with_body and self.body:
bits.append('\n')
if with_body and self.body: @property
if pretty and self.content_type: def headers(self):
bits.append(prettifier.process_body( """Return a `str` with the message's headers."""
self.body, self.content_type)) raise NotImplementedError()
else:
bits.append(self.body)
return '\n'.join(bit.strip() for bit in bits) @property
def encoding(self):
"""Return a `str` with the message's encoding, if known."""
raise NotImplementedError()
@staticmethod @property
def from_request(request): def body(self):
"""Make an `HTTPMessage` from `requests.models.Request`.""" """Return a `bytes` with the message's body."""
raise NotImplementedError()
url = urlparse(request.url) @property
def content_type(self):
"""Return the message content type."""
ct = self._orig.headers.get('Content-Type', '')
if isinstance(ct, bytes):
ct = ct.decode()
return ct
# Querystring
qs = ''
if url.query or request.params:
qs = '?'
if url.query:
qs += url.query
# Requests doesn't make params part of ``request.url``.
if request.params:
if url.query:
qs += '&'
#noinspection PyUnresolvedReferences
qs += type(request)._encode_params(request.params)
# Request-Line class HTTPResponse(HTTPMessage):
request_line = '{method} {path}{query} HTTP/1.1'.format( """A :class:`requests.models.Response` wrapper."""
method=request.method,
path=url.path or '/',
query=qs
)
# Headers def iter_body(self, chunk_size=1):
headers = dict(request.headers) return self._orig.iter_content(chunk_size=chunk_size)
content_type = headers.get('Content-Type')
if 'Host' not in headers:
headers['Host'] = url.netloc
headers = '\n'.join(
str('%s: %s') % (name, value)
for name, value
in headers.items()
)
# Body def iter_lines(self, chunk_size):
try: return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
body = request.data
except AttributeError:
# requests < 0.12.1
body = request._enc_data
if isinstance(body, dict):
#noinspection PyUnresolvedReferences
body = type(request)._encode_params(body)
return HTTPMessage( @property
line=request_line, def headers(self):
headers=headers, original = self._orig.raw._original_response
body=body,
content_type=content_type
)
@classmethod
def from_response(cls, response):
"""Make an `HTTPMessage` from `requests.models.Response`."""
encoding = response.encoding or 'ISO-8859-1'
original = response.raw._original_response
response_headers = response.headers
status_line = 'HTTP/{version} {status} {reason}'.format( status_line = 'HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)), version='.'.join(str(original.version)),
status=original.status, status=original.status,
reason=original.reason reason=original.reason
) )
body = response.content.decode(encoding) if response.content else '' headers = [status_line]
return cls( try:
line=status_line, # `original.msg` is a `http.client.HTTPMessage` on Python 3
headers=str(original.msg), # `_headers` is a 2-tuple
body=body, headers.extend(
content_type=response_headers.get('Content-Type')) '%s: %s' % header for header in original.msg._headers)
except AttributeError:
# and a `httplib.HTTPMessage` on Python 2.x
# `headers` is a list of `name: val<CRLF>`.
headers.extend(h.strip() for h in original.msg.headers)
return '\r\n'.join(headers)
@property
def encoding(self):
return self._orig.encoding or 'utf8'
@property
def body(self):
# Only now the response body is fetched.
# Shouldn't be touched unless the body is actually needed.
return self._orig.content
class HTTPRequest(HTTPMessage):
"""A :class:`requests.models.Request` wrapper."""
def iter_body(self, chunk_size):
yield self.body
def iter_lines(self, chunk_size):
yield self.body, b''
@property
def headers(self):
url = urlsplit(self._orig.url)
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method,
path=url.path or '/',
query='?' + url.query if url.query else ''
)
headers = dict(self._orig.headers)
if 'Host' not in headers:
headers['Host'] = url.netloc
headers = ['%s: %s' % (name, value)
for name, value in headers.items()]
headers.insert(0, request_line)
return '\r\n'.join(headers).strip()
@property
def encoding(self):
return 'utf8'
@property
def body(self):
body = self._orig.body
if isinstance(body, str):
# Happens with JSON/form request data parsed from the command line.
body = body.encode('utf8')
return body or b''

View File

@ -1,31 +1,303 @@
"""Output processing and formatting. """Output streaming, processing and formatting.
""" """
import re
import json import json
from functools import partial
from itertools import chain
import pygments import pygments
from pygments import token, lexer from pygments import token, lexer
from pygments.styles import get_style_by_name, STYLE_MAP from pygments.styles import get_style_by_name, STYLE_MAP
from pygments.lexers import get_lexer_for_mimetype from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound from pygments.util import ClassNotFound
from requests.compat import is_windows
from . import solarized from .compat import is_windows
from .solarized import Solarized256Style
from .models import HTTPRequest, HTTPResponse, Environment
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY)
DEFAULT_STYLE = 'solarized' # Colors on Windows via colorama don't look that
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys()) # great and fruity seems to give the best result there.
AVAILABLE_STYLES = set(STYLE_MAP.keys())
AVAILABLE_STYLES.add('solarized')
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
BINARY_SUPPRESSED_NOTICE = (
b'\n'
b'+-----------------------------------------+\n'
b'| NOTE: binary data not shown in terminal |\n'
b'+-----------------------------------------+'
)
class BinarySuppressedError(Exception):
"""An error indicating that the body is binary and won't be written,
e.g., for terminal output)."""
message = BINARY_SUPPRESSED_NOTICE
###############################################################################
# Output Streams
###############################################################################
def write(stream, outfile, flush):
"""Write the output stream."""
try:
# Writing bytes so we use the buffer interface (Python 3).
buf = outfile.buffer
except AttributeError:
buf = outfile
for chunk in stream:
buf.write(chunk)
if flush:
outfile.flush()
def write_with_colors_win_p3k(stream, outfile, flush):
"""Like `write`, but colorized chunks are written as text
directly to `outfile` to ensure it gets processed by colorama.
Applies only to Windows with Python 3 and colorized terminal output.
"""
color = b'\x1b['
encoding = outfile.encoding
for chunk in stream:
if color in chunk:
outfile.write(chunk.decode(encoding))
else:
outfile.buffer.write(chunk)
if flush:
outfile.flush()
def build_output_stream(args, env, request, response):
"""Build and return a chain of iterators over the `request`-`response`
exchange each of which yields `bytes` chunks.
"""
req_h = OUT_REQ_HEAD in args.output_options
req_b = OUT_REQ_BODY in args.output_options
resp_h = OUT_RESP_HEAD in args.output_options
resp_b = OUT_RESP_BODY in args.output_options
req = req_h or req_b
resp = resp_h or resp_b
output = []
Stream = get_stream_type(env, args)
if req:
output.append(Stream(
msg=HTTPRequest(request),
with_headers=req_h,
with_body=req_b))
if req_b and resp:
# Request/Response separator.
output.append([b'\n\n'])
if resp:
output.append(Stream(
msg=HTTPResponse(response),
with_headers=resp_h,
with_body=resp_b))
if env.stdout_isatty and resp_b:
# Ensure a blank line after the response body.
# For terminal output only.
output.append([b'\n\n'])
return chain(*output)
def get_stream_type(env, args):
"""Pick the right stream type based on `env` and `args`.
Wrap it in a partial with the type-specific args so that
we don't need to think what stream we are dealing with.
"""
if not env.stdout_isatty and not args.prettify:
Stream = partial(
RawStream,
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
if args.stream
else RawStream.CHUNK_SIZE
)
elif args.prettify:
Stream = partial(
PrettyStream if args.stream else BufferedPrettyStream,
env=env,
processor=OutputProcessor(
env=env, groups=args.prettify, pygments_style=args.style),
)
else:
Stream = partial(EncodedStream, env=env)
return Stream
class BaseStream(object):
"""Base HTTP message output stream class."""
def __init__(self, msg, with_headers=True, with_body=True):
"""
:param msg: a :class:`models.HTTPMessage` subclass
:param with_headers: if `True`, headers will be included
: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
def _get_headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('ascii')
def _iter_body(self):
"""Return an iterator over the message body."""
raise NotImplementedError()
def __iter__(self):
"""Return an iterator over `self.msg`."""
if self.with_headers:
yield self._get_headers()
yield b'\r\n\r\n'
if self.with_body:
try:
for chunk in self._iter_body():
yield chunk
except BinarySuppressedError as e:
if self.with_headers:
yield b'\n'
yield e.message
class RawStream(BaseStream):
"""The message is streamed in chunks with no processing."""
CHUNK_SIZE = 1024 * 100
CHUNK_SIZE_BY_LINE = 1024 * 5
def __init__(self, chunk_size=CHUNK_SIZE, **kwargs):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def _iter_body(self):
return self.msg.iter_body(self.chunk_size)
class EncodedStream(BaseStream):
"""Encoded HTTP message stream.
The message bytes are converted to an encoding suitable for
`self.env.stdout`. Unicode errors are replaced and binary data
is suppressed. The body is always streamed by line.
"""
CHUNK_SIZE = 1024 * 5
def __init__(self, env=Environment(), **kwargs):
super(EncodedStream, self).__init__(**kwargs)
if env.stdout_isatty:
# Use the encoding supported by the terminal.
output_encoding = getattr(env.stdout, 'encoding', None)
else:
# Preserve the message encoding.
output_encoding = self.msg.encoding
# Default to utf8 when unsure.
self.output_encoding = output_encoding or 'utf8'
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
yield line.decode(self.msg.encoding)\
.encode(self.output_encoding, 'replace') + lf
class PrettyStream(EncodedStream):
"""In addition to :class:`EncodedStream` behaviour, this stream applies
content processing.
Useful for long-lived HTTP responses that stream by lines
such as the Twitter streaming API.
"""
CHUNK_SIZE = 1024 * 5
def __init__(self, processor, **kwargs):
super(PrettyStream, self).__init__(**kwargs)
self.processor = processor
def _get_headers(self):
return self.processor.process_headers(
self.msg.headers).encode(self.output_encoding)
def _iter_body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
yield self._process_body(line) + lf
def _process_body(self, chunk):
return (self.processor
.process_body(
chunk.decode(self.msg.encoding, 'replace'),
self.msg.content_type)
.encode(self.output_encoding, 'replace'))
class BufferedPrettyStream(PrettyStream):
"""The same as :class:`PrettyStream` except that the body is fully
fetched before it's processed.
Suitable regular HTTP responses.
"""
CHUNK_SIZE = 1024 * 10
def _iter_body(self):
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
body = bytearray()
for chunk in self.msg.iter_body(self.CHUNK_SIZE):
if b'\0' in chunk:
raise BinarySuppressedError()
body.extend(chunk)
yield self._process_body(body)
###############################################################################
# Processing
###############################################################################
class HTTPLexer(lexer.RegexLexer): class HTTPLexer(lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments. """Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between It only operates on headers and provides a stronger contrast between
their names and values than the original one bundled with Pygments their names and values than the original one bundled with Pygments
(`pygments.lexers.text import HttpLexer`), especially when (:class:`pygments.lexers.text import HttpLexer`), especially when
Solarized color scheme is used. Solarized color scheme is used.
""" """
@ -34,7 +306,6 @@ class HTTPLexer(lexer.RegexLexer):
filenames = ['*.http'] filenames = ['*.http']
tokens = { tokens = {
'root': [ 'root': [
# Request-Line # Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)', (r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
lexer.bygroups( lexer.bygroups(
@ -46,7 +317,6 @@ class HTTPLexer(lexer.RegexLexer):
token.Operator, token.Operator,
token.Number token.Number
)), )),
# Response Status-Line # Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)', (r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
lexer.bygroups( lexer.bygroups(
@ -58,7 +328,6 @@ class HTTPLexer(lexer.RegexLexer):
token.Text, token.Text,
token.Name.Exception, # Reason token.Name.Exception, # Reason
)), )),
# Header # Header
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups( (r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
token.Name.Attribute, # Name token.Name.Attribute, # Name
@ -67,47 +336,73 @@ class HTTPLexer(lexer.RegexLexer):
token.Text, token.Text,
token.String # Value token.String # Value
)) ))
]} ]
}
class BaseProcessor(object): class BaseProcessor(object):
"""Base, noop output processor class."""
enabled = True enabled = True
def __init__(self, env, **kwargs): def __init__(self, env=Environment(), **kwargs):
"""
:param env: an class:`Environment` instance
:param kwargs: additional keyword argument that some
processor might require.
"""
self.env = env self.env = env
self.kwargs = kwargs self.kwargs = kwargs
def process_headers(self, headers): def process_headers(self, headers):
"""Return processed `headers`
:param headers: The headers as text.
"""
return headers return headers
def process_body(self, content, content_type): def process_body(self, content, content_type, subtype):
"""Return processed `content`.
:param content: The body content as text
:param content_type: Full content type, e.g., 'application/atom+xml'.
:param subtype: E.g. 'xml'.
"""
return content return content
class JSONProcessor(BaseProcessor): class JSONProcessor(BaseProcessor):
"""JSON body processor."""
def process_body(self, content, content_type): def process_body(self, content, content_type, subtype):
if content_type == 'application/json': if subtype == 'json':
try: try:
# Indent and sort the JSON data. # Indent the JSON data, sort keys by name, and
content = json.dumps( # avoid unicode escapes to improve readability.
json.loads(content), content = json.dumps(json.loads(content),
sort_keys=True, sort_keys=True,
ensure_ascii=False, ensure_ascii=False,
indent=4, indent=4)
)
except ValueError: except ValueError:
# Invalid JSON - we don't care. # Invalid JSON but we don't care.
pass pass
return content return content
class PygmentsProcessor(BaseProcessor): class PygmentsProcessor(BaseProcessor):
"""A processor that applies syntax-highlighting using Pygments
to the headers, and to the body as well if its content type is recognized.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PygmentsProcessor, self).__init__(*args, **kwargs) super(PygmentsProcessor, self).__init__(*args, **kwargs)
# Cache that speeds up when we process streamed body by line.
self.lexers_by_type = {}
if not self.env.colors: if not self.env.colors:
self.enabled = False self.enabled = False
return return
@ -116,9 +411,9 @@ class PygmentsProcessor(BaseProcessor):
style = get_style_by_name( style = get_style_by_name(
self.kwargs.get('pygments_style', DEFAULT_STYLE)) self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound: except ClassNotFound:
style = solarized.SolarizedStyle style = Solarized256Style
if is_windows or self.env.colors == 256: if self.env.is_windows or self.env.colors == 256:
fmt_class = Terminal256Formatter fmt_class = Terminal256Formatter
else: else:
fmt_class = TerminalFormatter fmt_class = TerminalFormatter
@ -126,32 +421,61 @@ class PygmentsProcessor(BaseProcessor):
def process_headers(self, headers): def process_headers(self, headers):
return pygments.highlight( return pygments.highlight(
headers, HTTPLexer(), self.formatter) headers, HTTPLexer(), self.formatter).strip()
def process_body(self, content, content_type): def process_body(self, content, content_type, subtype):
try:
lexer = self.lexers_by_type.get(content_type)
if not lexer:
try: try:
lexer = get_lexer_for_mimetype(content_type) lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
lexer = get_lexer_by_name(subtype)
self.lexers_by_type[content_type] = lexer
except ClassNotFound: except ClassNotFound:
pass pass
else: else:
content = pygments.highlight(content, lexer, self.formatter) content = pygments.highlight(content, lexer, self.formatter)
return content return content.strip()
class HeadersProcessor(BaseProcessor):
"""Sorts headers by name retaining relative order of multiple headers
with the same name.
"""
def process_headers(self, headers):
lines = headers.splitlines()
headers = sorted(lines[1:], key=lambda h: h.split(':')[0])
return '\r\n'.join(lines[:1] + headers)
class OutputProcessor(object): class OutputProcessor(object):
"""A delegate class that invokes the actual processors."""
installed_processors = [ installed_processors = {
JSONProcessor, 'format': [
HeadersProcessor,
JSONProcessor
],
'colors': [
PygmentsProcessor PygmentsProcessor
] ]
}
def __init__(self, env, **kwargs): def __init__(self, groups, env=Environment(), **kwargs):
self.env = env """
processors = [ :param env: a :class:`models.Environment` instance
cls(env, **kwargs) :param groups: the groups of processors to be applied
for cls in self.installed_processors :param kwargs: additional keyword arguments for processors
]
self.processors = [p for p in processors if p.enabled] """
self.processors = []
for group in groups:
for cls in self.installed_processors[group]:
processor = cls(env, **kwargs)
if processor.enabled:
self.processors.append(processor)
def process_headers(self, headers): def process_headers(self, headers):
for processor in self.processors: for processor in self.processors:
@ -159,18 +483,12 @@ class OutputProcessor(object):
return headers return headers
def process_body(self, content, content_type): def process_body(self, content, content_type):
# e.g., 'application/atom+xml'
content_type = content_type.split(';')[0] content_type = content_type.split(';')[0]
# e.g., 'xml'
application_match = re.match( subtype = content_type.split('/')[-1].split('+')[-1]
r'application/(.+\+)(json|xml)$',
content_type
)
if application_match:
# Strip vendor and extensions from Content-Type
vendor, extension = application_match.groups()
content_type = content_type.replace(vendor, '')
for processor in self.processors: for processor in self.processors:
content = processor.process_body(content, content_type) content = processor.process_body(content, content_type, subtype)
return content return content

179
httpie/sessions.py Normal file
View File

@ -0,0 +1,179 @@
"""Persistent, JSON-serialized sessions.
"""
import re
import os
import glob
import errno
import shutil
import requests
from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from .compat import urlsplit
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
SESSIONS_DIR_NAME = 'sessions'
DEFAULT_SESSIONS_DIR = os.path.join(DEFAULT_CONFIG_DIR, SESSIONS_DIR_NAME)
def get_response(name, request_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=request_kwargs['headers'].get('Host', None)
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
)
session = Session(host, name)
session.load()
# Update session headers with the request headers.
session['headers'].update(request_kwargs.get('headers', {}))
# Use the merged headers for the request
request_kwargs['headers'] = session['headers']
auth = request_kwargs.get('auth', None)
if auth:
session.auth = auth
elif session.auth:
request_kwargs['auth'] = session.auth
requests_session = requests.Session()
requests_session.cookies = session.cookies
try:
response = requests_session.request(**request_kwargs)
except Exception:
raise
else:
# Existing sessions with `read_only=True` don't get updated.
if session.is_new or not read_only:
session.cookies = requests_session.cookies
session.save()
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'
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
self['headers'] = {}
self['cookies'] = {}
self['auth'] = {
'type': None,
'username': None,
'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)
@property
def cookies(self):
jar = RequestsCookieJar()
for name, cookie_dict in self['cookies'].items():
jar.set_cookie(create_cookie(
name, cookie_dict.pop('value'), **cookie_dict))
jar.clear_expired_cookies()
return jar
@cookies.setter
def cookies(self, jar):
# http://docs.python.org/2/library/cookielib.html#cookie-objects
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for host in jar._cookies.values():
for path in host.values():
for name, cookie in path.items():
self['cookies'][name] = dict(
(attname, getattr(cookie, attname))
for attname in stored_attrs
)
@property
def auth(self):
auth = self.get('auth', None)
if not auth or not auth['type']:
return
Auth = {'basic': HTTPBasicAuth,
'digest': HTTPDigestAuth}[auth['type']]
return Auth(auth['username'], auth['password'])
@auth.setter
def auth(self, cred):
self['auth'] = {
'type': {HTTPBasicAuth: 'basic',
HTTPDigestAuth: 'digest'}[type(cred)],
'username': cred.username,
'password': cred.password,
}

View File

@ -1,74 +1,57 @@
""" # -*- coding: utf-8 -*-
A Pygments_ style based on the dark background variant of Solarized_.
.. _Pygments: http://pygments.org/
.. _Solarized: http://ethanschoonover.com/solarized
Copyright (c) 2011 Hank Gay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
""" """
solarized256
------------
A Pygments style inspired by Solarized's 256 color mode.
:copyright: (c) 2011 by Hank Gay, (c) 2012 by John Mastro.
:license: BSD, see LICENSE for more details.
"""
from pygments.style import Style from pygments.style import Style
from pygments.token import (Token, Comment, Name, Keyword, Generic, Number, from pygments.token import Token, Comment, Name, Keyword, Generic, Number, \
Operator, String) Operator, String
BASE03 = "#1c1c1c"
BASE02 = "#262626"
BASE01 = "#4e4e4e"
BASE00 = "#585858"
BASE0 = "#808080"
BASE1 = "#8a8a8a"
BASE2 = "#d7d7af"
BASE3 = "#ffffd7"
YELLOW = "#af8700"
ORANGE = "#d75f00"
RED = "#af0000"
MAGENTA = "#af005f"
VIOLET = "#5f5faf"
BLUE = "#0087ff"
CYAN = "#00afaf"
GREEN = "#5f8700"
BASE03 = '#002B36' class Solarized256Style(Style):
BASE02 = '#073642'
BASE01 = '#586E75'
BASE00 = '#657B83'
BASE0 = '#839496'
BASE1 = '#93A1A1'
BASE2 = '#EEE8D5'
BASE3 = '#FDF6E3'
YELLOW = '#B58900'
ORANGE = '#CB4B16'
RED = '#DC322F'
MAGENTA = '#D33682'
VIOLET = '#6C71C4'
BLUE = '#268BD2'
CYAN = '#2AA198'
GREEN = '#859900'
class SolarizedStyle(Style):
background_color = BASE03 background_color = BASE03
styles = { styles = {
Keyword: GREEN, Keyword: GREEN,
Keyword.Constant: ORANGE, Keyword.Constant: ORANGE,
Keyword.Declaration: BLUE, Keyword.Declaration: BLUE,
#Keyword.Namespace Keyword.Namespace: ORANGE,
#Keyword.Pseudo #Keyword.Pseudo
Keyword.Reserved: BLUE, Keyword.Reserved: BLUE,
Keyword.Type: RED, Keyword.Type: RED,
#Name #Name
Name.Attribute: BASE1, Name.Attribute: BASE1,
Name.Builtin: YELLOW, Name.Builtin: BLUE,
Name.Builtin.Pseudo: BLUE, Name.Builtin.Pseudo: BLUE,
Name.Class: BLUE, Name.Class: BLUE,
Name.Constant: ORANGE, Name.Constant: ORANGE,
Name.Decorator: BLUE, Name.Decorator: BLUE,
Name.Entity: ORANGE, Name.Entity: ORANGE,
Name.Exception: ORANGE, Name.Exception: YELLOW,
Name.Function: BLUE, Name.Function: BLUE,
#Name.Label #Name.Label
#Name.Namespace #Name.Namespace
@ -84,10 +67,10 @@ class SolarizedStyle(Style):
String: CYAN, String: CYAN,
String.Backtick: BASE01, String.Backtick: BASE01,
String.Char: CYAN, String.Char: CYAN,
String.Doc: BASE1, String.Doc: CYAN,
#String.Double #String.Double
String.Escape: ORANGE, String.Escape: RED,
String.Heredoc: BASE1, String.Heredoc: CYAN,
#String.Interpol #String.Interpol
#String.Other #String.Other
String.Regex: RED, String.Regex: RED,
@ -100,8 +83,8 @@ class SolarizedStyle(Style):
#Number.Integer.Long #Number.Integer.Long
#Number.Oct #Number.Oct
Operator: GREEN, Operator: BASE1,
#Operator.Word Operator.Word: GREEN,
#Punctuation: ORANGE, #Punctuation: ORANGE,

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
#

View File

@ -1,5 +1,7 @@
import os import os
import sys import sys
import re
import codecs
from setuptools import setup from setuptools import setup
import httpie import httpie
@ -10,8 +12,7 @@ if sys.argv[-1] == 'test':
requirements = [ requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that. 'requests>=1.0.4',
'requests>=0.10.1',
'Pygments>=1.5' 'Pygments>=1.5'
] ]
if sys.version_info[:2] in ((2, 6), (3, 1)): if sys.version_info[:2] in ((2, 6), (3, 1)):
@ -22,11 +23,20 @@ if 'win32' in str(sys.platform).lower():
requirements.append('colorama>=0.2.4') requirements.append('colorama>=0.2.4')
def long_description():
"""Pre-process the README so that PyPi can render it properly."""
with codecs.open('README.rst', encoding='utf8') as f:
rst = f.read()
code_block = '(:\n\n)?\.\. code-block::.*'
rst = re.sub(code_block, '::', rst)
return rst
setup( setup(
name='httpie', name='httpie',
version=httpie.__version__, version=httpie.__version__,
description=httpie.__doc__.strip(), description=httpie.__doc__.strip(),
long_description=open('README.rst').read(), long_description=long_description(),
url='http://httpie.org/', url='http://httpie.org/',
download_url='https://github.com/jkbr/httpie', download_url='https://github.com/jkbr/httpie',
author=httpie.__author__, author=httpie.__author__,
@ -36,6 +46,7 @@ setup(
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'http = httpie.__main__:main', 'http = httpie.__main__:main',
'httpie = httpie.manage:main',
], ],
}, },
install_requires=requirements, install_requires=requirements,

BIN
tests/fixtures/file.bin vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = py26, py27, py32, pypy envlist = py26, py27, py33, pypy
[testenv] [testenv]
commands = {envpython} setup.py test commands = {envpython} setup.py test