mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 18:34:53 +02:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
5945845420 | |||
3ee5b49256 | |||
bb024757b6 | |||
d35864e79d | |||
8a106781be | |||
23dd80563f | |||
2bab69d9fb | |||
826489950d | |||
b86598886e | |||
c240162cab | |||
26e29612f2 | |||
37200eb055 | |||
9c68d7dd87 | |||
7ee519ef46 | |||
c4627cc882 | |||
492687b0da | |||
caeef2fb7c | |||
aae596d472 | |||
cb51faec51 | |||
c2a0cef76e | |||
493e98c833 | |||
ca02e51420 | |||
cd085cbc0d | |||
27d57ce773 | |||
4c4efff56a | |||
a53505f26e | |||
165dc36f8d | |||
5df3a91619 | |||
7dbceafc01 | |||
d62d6a77d1 | |||
0a81facccf | |||
3e20ade645 | |||
0c47094109 | |||
defe4bc76d | |||
afee6a7970 | |||
7b676dd583 | |||
5af0874ed3 | |||
e11a2d1346 | |||
b2044fc18d | |||
d9a2d665ad | |||
e83e275dff | |||
4a99495466 | |||
495f67229a | |||
45b9bae3dc | |||
774ff148cd |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: jakubroztocil # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,7 +7,7 @@ htmlcov
|
||||
|
||||
|
||||
##############################################################################
|
||||
# The bellow is GitHub template for Python project. gitignore.
|
||||
# The below is GitHub template for Python project. gitignore.
|
||||
# <https://github.com/github/gitignore/blob/master/Python.gitignore>
|
||||
##############################################################################
|
||||
|
||||
|
@ -6,16 +6,30 @@ This document records all notable changes to `HTTPie <https://httpie.org>`_.
|
||||
This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
|
||||
|
||||
`2.2.0`_ (2020-06-18)
|
||||
-------------------------
|
||||
|
||||
* Added support for custom content types for uploaded files (`#668`_).
|
||||
* Added support for ``$XDG_CONFIG_HOME`` (`#920`_).
|
||||
* Added support for ``Set-Cookie``-triggered cookie expiration (`#853`_).
|
||||
* Added ``--format-options`` to allow disabling sorting, etc. (`#128`_)
|
||||
* Added ``--sorted`` and ``--unsorted`` shortcuts for (un)setting all sorting-related ``--format-options``. (`#128`_)
|
||||
* Added ``--ciphers`` to allow configuring OpenSSL ciphers (`#870`_).
|
||||
* Added ``netrc`` support for auth plugins. Enabled for ``--auth-type=basic``
|
||||
and ``digest``, 3rd parties may opt in (`#718`_, `#719`_, `#852`_, `#934`_).
|
||||
* Fixed built-in plugins-related circular imports (`#925`_).
|
||||
|
||||
|
||||
`2.1.0`_ (2020-04-18)
|
||||
---------------------
|
||||
|
||||
* Added ``--path-as-is`` to bypass dot segment (``/../`` or ``/./``)
|
||||
URL squashing (#895).
|
||||
* Changed the default value ``Accept`` header value for JSON requests from
|
||||
URL squashing (`#895`_).
|
||||
* Changed the default ``Accept`` header value for JSON requests from
|
||||
``application/json, */*`` to ``application/json, */*;q=0.5``
|
||||
to clearly indicate preference (#488).
|
||||
to clearly indicate preference (`#488`_).
|
||||
* Fixed ``--form`` file upload mixed with redirected ``stdin`` error handling
|
||||
(#840).
|
||||
(`#840`_).
|
||||
|
||||
|
||||
`2.0.0`_ (2020-01-12)
|
||||
@ -125,7 +139,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
``$ alias https='http --default-scheme=https``.
|
||||
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
|
||||
* Added fish shell completion (located in ``extras/httpie-completion.fish``
|
||||
in the Github repo).
|
||||
in the GitHub repo).
|
||||
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
|
||||
``pip install requests[socks]``.
|
||||
* Changed the default JSON ``Accept`` header from ``application/json``
|
||||
@ -422,8 +436,19 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
.. _1.0.3: https://github.com/jakubroztocil/httpie/compare/1.0.2...1.0.3
|
||||
.. _2.0.0: https://github.com/jakubroztocil/httpie/compare/1.0.3...2.0.0
|
||||
.. _2.1.0: https://github.com/jakubroztocil/httpie/compare/2.0.0...2.1.0
|
||||
.. _2.2.0: https://github.com/jakubroztocil/httpie/compare/2.1.0...2.2.0
|
||||
|
||||
|
||||
.. _#488:https://github.com/jakubroztocil/httpie/issues/488
|
||||
.. _#840:https://github.com/jakubroztocil/httpie/issues/840
|
||||
.. _#895:https://github.com/jakubroztocil/httpie/issues/895
|
||||
.. _#128: https://github.com/jakubroztocil/httpie/issues/128
|
||||
.. _#488: https://github.com/jakubroztocil/httpie/issues/488
|
||||
.. _#668: https://github.com/jakubroztocil/httpie/issues/668
|
||||
.. _#718: https://github.com/jakubroztocil/httpie/issues/718
|
||||
.. _#719: https://github.com/jakubroztocil/httpie/issues/719
|
||||
.. _#840: https://github.com/jakubroztocil/httpie/issues/840
|
||||
.. _#853: https://github.com/jakubroztocil/httpie/issues/853
|
||||
.. _#852: https://github.com/jakubroztocil/httpie/issues/852
|
||||
.. _#870: https://github.com/jakubroztocil/httpie/issues/870
|
||||
.. _#895: https://github.com/jakubroztocil/httpie/issues/895
|
||||
.. _#920: https://github.com/jakubroztocil/httpie/issues/920
|
||||
.. _#925: https://github.com/jakubroztocil/httpie/issues/925
|
||||
.. _#934: https://github.com/jakubroztocil/httpie/issues/934
|
||||
|
@ -115,7 +115,7 @@ Testing & CI
|
||||
Please add tests for any new features and bug fixes.
|
||||
|
||||
When you open a pull request,
|
||||
`Github Actions <https://github.com/jakubroztocil/httpie/actions>`_
|
||||
`GitHub Actions <https://github.com/jakubroztocil/httpie/actions>`_
|
||||
will automatically run HTTPie’s `test suite`_ against your code
|
||||
so please make sure all checks pass.
|
||||
|
||||
|
116
README.rst
116
README.rst
@ -11,7 +11,7 @@ generally interacting with HTTP servers.
|
||||
|
||||
.. class:: no-web no-pdf
|
||||
|
||||
|pypi| |build| |coverage| |downloads| |gitter|
|
||||
|docs| |pypi| |build| |coverage| |downloads| |gitter|
|
||||
|
||||
|
||||
.. class:: no-web no-pdf
|
||||
@ -228,7 +228,7 @@ Build and print a request without sending it using `offline mode`_:
|
||||
$ http --offline httpbin.org/post hello=offline
|
||||
|
||||
|
||||
Use `Github API`_ to post a comment on an
|
||||
Use `GitHub API`_ to post a comment on an
|
||||
`issue <https://github.com/jakubroztocil/httpie/issues/83>`_
|
||||
with `authentication`_:
|
||||
|
||||
@ -305,8 +305,23 @@ Request URL
|
||||
===========
|
||||
|
||||
The only information HTTPie needs to perform a request is a URL.
|
||||
The default scheme is, somewhat unsurprisingly, ``http://``,
|
||||
and can be omitted from the argument – ``http example.org`` works just fine.
|
||||
|
||||
The default scheme is ``http://`` and can be omitted from the argument:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http example.org
|
||||
# => http://example.org
|
||||
|
||||
|
||||
HTTPie also installs an ``https`` executable, where the default
|
||||
scheme is ``https://``:
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ https example.org
|
||||
# => https://example.org
|
||||
|
||||
|
||||
Querystring parameters
|
||||
@ -668,6 +683,13 @@ submitted:
|
||||
Note that ``@`` is used to simulate a file upload form field, whereas
|
||||
``=@`` just embeds the file content as a regular text field value.
|
||||
|
||||
When uploading files, their content type is inferred from the file name. You can manually
|
||||
override the inferred content type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http -f POST httpbin.org/post name='John Smith' cv@'~/files/data.bin;type=application/pdf'
|
||||
|
||||
|
||||
HTTP headers
|
||||
============
|
||||
@ -707,7 +729,7 @@ There are a couple of default headers that HTTPie sets:
|
||||
|
||||
|
||||
|
||||
Any of these except ``Host`` can be overwritten and some of them unset.
|
||||
Any of these can be overwritten and some of them unset (see bellow).
|
||||
|
||||
|
||||
|
||||
@ -1092,11 +1114,13 @@ path of the key file with ``--cert-key``:
|
||||
SSL version
|
||||
-----------
|
||||
|
||||
Use the ``--ssl=<PROTOCOL>`` to specify the desired protocol version to use.
|
||||
This will default to SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. The available protocols
|
||||
are ``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ``tls1.2``, ``tls1.3``. (The actually
|
||||
available set of protocols may vary depending on your OpenSSL installation.)
|
||||
Use the ``--ssl=<PROTOCOL>`` option to specify the desired protocol version to
|
||||
use. This will default to SSL v2.3 which will negotiate the highest protocol
|
||||
that both the server and your installation of OpenSSL support. The available
|
||||
protocols are
|
||||
``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ``tls1.2``, ``tls1.3``.
|
||||
(The actually available set of protocols may vary depending on your OpenSSL
|
||||
installation.)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -1104,6 +1128,26 @@ available set of protocols may vary depending on your OpenSSL installation.)
|
||||
$ http --ssl=ssl3 https://vulnerable.example.org
|
||||
|
||||
|
||||
|
||||
SSL ciphers
|
||||
-----------
|
||||
|
||||
You can specify the available ciphers with ``--ciphers``.
|
||||
It should be a string in the
|
||||
`OpenSSL cipher list format <https://www.openssl.org/docs/man1.1.0/man1/ciphers.html>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http --ciphers=ECDHE-RSA-AES128-GCM-SHA256 https://httpbin.org/get
|
||||
|
||||
Note: these cipher strings do not change the negotiated version of SSL or TLS,
|
||||
they only affect the list of available cipher suites.
|
||||
|
||||
To see the default cipher string, run ``http --help`` and see
|
||||
the ``--ciphers`` section under SSL.
|
||||
|
||||
|
||||
|
||||
Output options
|
||||
==============
|
||||
|
||||
@ -1358,6 +1402,26 @@ One of these options can be used to control output processing:
|
||||
Default for redirected output.
|
||||
==================== ========================================================
|
||||
|
||||
|
||||
You can control the applied formatting via the ``--format-options`` option.
|
||||
The following options are available:
|
||||
|
||||
For example, this is how you would disable the default header and JSON key
|
||||
sorting, and specify a custom JSON indent size:
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http --format-options headers.sort:false,json.sort_keys:false,json.indent:2 httpbin.org/get
|
||||
|
||||
This is something you will typically store as one of the default options in your
|
||||
`config`_ file. See ``http --help`` for all the available formatting options.
|
||||
|
||||
There are also two shortcuts that allow you to quickly disable and re-enable
|
||||
sorting-related format options (currently it means JSON keys and headers):
|
||||
``--unsorted`` and ``--sorted``.
|
||||
|
||||
|
||||
Binary data
|
||||
-----------
|
||||
|
||||
@ -1621,9 +1685,9 @@ To create or reuse a different session, simple specify a different name:
|
||||
|
||||
$ http --session=user2 -a user2:password httpbin.org/get X-Bar:Foo
|
||||
|
||||
Named sessions’s data is stored in JSON files in the the ``sessions``
|
||||
subdirectory of the `config`_ directory:
|
||||
``~/.httpie/sessions/<host>/<name>.json``
|
||||
Named sessions’s data is stored in JSON files inside the ``sessions``
|
||||
subdirectory of the `config`_ directory, typically:
|
||||
``~/.config/httpie/sessions/<host>/<name>.json``
|
||||
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||
|
||||
If you have executed the above commands on a unix machine,
|
||||
@ -1632,7 +1696,7 @@ you should be able list the generated sessions files using:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ ls -l ~/.httpie/sessions/httpbin.org
|
||||
$ ls -l ~/.config/httpie/sessions/httpbin.org
|
||||
|
||||
|
||||
Anonymous sessions
|
||||
@ -1655,7 +1719,7 @@ allows for sessions to be re-used across multiple hosts:
|
||||
.. code-block:: bash
|
||||
|
||||
# You can also refer to a previously created named session:
|
||||
$ http --session=~/.httpie/sessions/another.example.org/test.json example.org
|
||||
$ http --session=~/.config/httpie/sessions/another.example.org/test.json example.org
|
||||
|
||||
|
||||
When creating anonymous sessions, please remember to always include at least
|
||||
@ -1691,8 +1755,17 @@ but you can create it manually.
|
||||
Config file directory
|
||||
---------------------
|
||||
|
||||
The default location of the configuration file is ``~/.httpie/config.json``
|
||||
(or ``%APPDATA%\httpie\config.json`` on Windows).
|
||||
To see the exact location for your installation, run ``http --debug`` and
|
||||
look for ``config_dir`` in the output.
|
||||
|
||||
The default location of the configuration file on most platforms is
|
||||
``$XDG_CONFIG_HOME/httpie/config.json`` (defaulting to
|
||||
``~/.config/httpie/config.json``).
|
||||
|
||||
For backwards compatibility, if the directory ``~/.httpie`` exists,
|
||||
the configuration file there will be used instead.
|
||||
|
||||
On Windows, the config file is located at ``%APPDATA%\httpie\config.json``.
|
||||
|
||||
The config directory can be changed by setting the ``$HTTPIE_CONFIG_DIR``
|
||||
environment variable:
|
||||
@ -1702,7 +1775,6 @@ environment variable:
|
||||
$ export HTTPIE_CONFIG_DIR=/tmp/httpie
|
||||
$ http httpbin.org/get
|
||||
|
||||
To view the exact location run ``http --debug``.
|
||||
|
||||
|
||||
Configurable options
|
||||
@ -1722,7 +1794,7 @@ For instance, you can use this config option to change your default color theme:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cat ~/.httpie/config.json
|
||||
$ cat ~/.config/httpie/config.json
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
@ -1933,12 +2005,16 @@ have contributed.
|
||||
|
||||
|
||||
.. _pip: https://pip.pypa.io/en/stable/installing/
|
||||
.. _Github API: https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||
.. _GitHub API: https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||
.. _these fine people: https://github.com/jakubroztocil/httpie/contributors
|
||||
.. _Jakub Roztocil: https://roztocil.co
|
||||
.. _@jakubroztocil: https://twitter.com/jakubroztocil
|
||||
|
||||
|
||||
.. |docs| image:: https://img.shields.io/badge/stable%20docs-httpie.org%2Fdocs-brightgreen?style=flat-square
|
||||
:target: https://httpie.org/docs
|
||||
:alt: Stable documentation
|
||||
|
||||
.. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version
|
||||
:target: https://pypi.python.org/pypi/httpie
|
||||
:alt: Latest version released on PyPi
|
||||
|
@ -9,43 +9,42 @@ class Httpie < Formula
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.org/"
|
||||
url "https://files.pythonhosted.org/packages/35/6c/93da2ebd4eb768c3733437ce01b5fae297522434fdeabeeabdc4f42aabd3/httpie-2.0.0.tar.gz"
|
||||
sha256 "8c04f9756f1a7eac71a6dfa0834d0f6813dc8a982d8564f3a7418dcd19107c09"
|
||||
revision 1
|
||||
url "https://files.pythonhosted.org/packages/e2/79/da6aec7b4356e8b325561b987c940e5b1e4de1200a5c3db7c57a97d61ca1/httpie-2.1.0.tar.gz"
|
||||
sha256 "a76f1c72e83bd03cde3478c5f345d5570fdb2967ed19d68d09518088640b9e8e"
|
||||
head "https://github.com/jakubroztocil/httpie.git"
|
||||
|
||||
bottle do
|
||||
cellar :any_skip_relocation
|
||||
sha256 "19694b5ec311939a8b73cc329ca49386155ed3a17e4eca691779c725d36286b5" => :catalina
|
||||
sha256 "8c7d93d55ea3351e25fadfdd3748ca0a3ff7dd62ab9dbf31b7243fba76890c4d" => :mojave
|
||||
sha256 "e0d5269bb5d03a1797c8612005fa46d6a35f2b84eb76e9607ef1169464b566ea" => :high_sierra
|
||||
sha256 "1fb33d9c85dc462c2549a03cf08670edad8014a5fdf0a7cb26493c64af40283d" => :catalina
|
||||
sha256 "a22030f0b96c698c90265286ee80ffbb03079d1d008a80c0bdb3ea15a17d3fbb" => :mojave
|
||||
sha256 "9f994ecf826efe53a3a49d1c3193e271629068d11306df55adeea2842a8afb8c" => :high_sierra
|
||||
end
|
||||
|
||||
depends_on "python@3.8"
|
||||
|
||||
resource "Pygments" do
|
||||
url "https://files.pythonhosted.org/packages/cb/9f/27d4844ac5bf158a33900dbad7985951e2910397998e85712da03ce125f0/Pygments-2.5.2.tar.gz"
|
||||
sha256 "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
|
||||
url "https://files.pythonhosted.org/packages/6e/4d/4d2fe93a35dfba417311a4ff627489a947b01dc0cc377a3673c00cf7e4b2/Pygments-2.6.1.tar.gz"
|
||||
sha256 "647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/01/62/ddcf76d1d19885e8579acb1b1df26a852b03472c0e46d2b959a714c90608/requests-2.22.0.tar.gz"
|
||||
sha256 "11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"
|
||||
url "https://files.pythonhosted.org/packages/f5/4f/280162d4bd4d8aad241a21aecff7a6e46891b905a4341e7ab549ebaf7915/requests-2.23.0.tar.gz"
|
||||
sha256 "b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
end
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/41/bf/9d214a5af07debc6acf7f3f257265618f1db242a3f8e49a9b516f24523a6/certifi-2019.11.28.tar.gz"
|
||||
sha256 "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
url "https://files.pythonhosted.org/packages/b8/e2/a3a86a67c3fc8249ed305fc7b7d290ebe5e4d46ad45573884761ef4dea7b/certifi-2020.4.5.1.tar.gz"
|
||||
sha256 "51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/ad/fc/54d62fa4fc6e675678f9519e677dfc29b8964278d75333cf142892caf015/urllib3-1.25.7.tar.gz"
|
||||
sha256 "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
|
||||
url "https://files.pythonhosted.org/packages/05/8c/40cd6949373e23081b3ea20d5594ae523e681b6f472e600fbc95ed046a36/urllib3-1.25.9.tar.gz"
|
||||
sha256 "3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/ad/13/eb56951b6f7950cadb579ca166e448ba77f9d24efc03edd7e55fa57d04b7/idna-2.8.tar.gz"
|
||||
sha256 "c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"
|
||||
url "https://files.pythonhosted.org/packages/cb/19/57503b5de719ee45e83472f339f617b0c01ad75cba44aba1e4c97c2b0abd/idna-2.9.tar.gz"
|
||||
sha256 "7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"
|
||||
end
|
||||
|
||||
resource "chardet" do
|
||||
|
@ -3,6 +3,6 @@ HTTPie - a CLI, cURL-like tool for humans.
|
||||
|
||||
"""
|
||||
|
||||
__version__ = '2.1.0'
|
||||
__version__ = '2.2.0'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -7,9 +7,16 @@ from argparse import RawDescriptionHelpFormatter
|
||||
from textwrap import dedent
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from httpie.cli.argtypes import AuthCredentials, KeyValueArgType, parse_auth
|
||||
from requests.utils import get_netrc_auth
|
||||
|
||||
from httpie.cli.argtypes import (
|
||||
AuthCredentials, KeyValueArgType, PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
parse_auth,
|
||||
parse_format_options,
|
||||
)
|
||||
from httpie.cli.constants import (
|
||||
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
DEFAULT_FORMAT_OPTIONS, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT,
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED, OUT_RESP_BODY, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS,
|
||||
SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE,
|
||||
@ -18,7 +25,7 @@ from httpie.cli.constants import (
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from httpie.cli.requestitems import RequestItems
|
||||
from httpie.context import Environment
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.utils import ExplicitNullAuth, get_content_type
|
||||
|
||||
|
||||
@ -42,6 +49,8 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
return text.splitlines()
|
||||
|
||||
|
||||
# TODO: refactor and design type-annotated data structures
|
||||
# for raw args + parsed args and keep things immutable.
|
||||
class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
@ -82,6 +91,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self._setup_standard_streams()
|
||||
self._process_output_options()
|
||||
self._process_pretty_options()
|
||||
self._process_format_options()
|
||||
self._guess_method()
|
||||
self._parse_items()
|
||||
|
||||
@ -154,7 +164,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
def _process_auth(self):
|
||||
# TODO: refactor
|
||||
# TODO: refactor & simplify this method.
|
||||
self.args.auth_plugin = None
|
||||
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
|
||||
auth_type_set = self.args.auth_type is not None
|
||||
@ -177,6 +187,19 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self.args.auth_type = default_auth_plugin.auth_type
|
||||
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
|
||||
|
||||
if (not self.args.ignore_netrc
|
||||
and self.args.auth is None
|
||||
and plugin.netrc_parse):
|
||||
# Only host needed, so it’s OK URL not finalized.
|
||||
netrc_credentials = get_netrc_auth(self.args.url)
|
||||
if netrc_credentials:
|
||||
self.args.auth = AuthCredentials(
|
||||
key=netrc_credentials[0],
|
||||
value=netrc_credentials[1],
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=SEPARATOR_CREDENTIALS.join(netrc_credentials)
|
||||
)
|
||||
|
||||
if plugin.auth_require and self.args.auth is None:
|
||||
self.error('--auth required')
|
||||
|
||||
@ -390,3 +413,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
if self.args.download_resume and not (
|
||||
self.args.download and self.args.output_file):
|
||||
self.error('--continue requires --output to be specified')
|
||||
|
||||
def _process_format_options(self):
|
||||
parsed_options = PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
for options_group in self.args.format_options or []:
|
||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||
self.args.format_options = parsed_options
|
||||
|
@ -2,9 +2,10 @@ import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from typing import Union, List, Optional
|
||||
from copy import deepcopy
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from httpie.cli.constants import SEPARATOR_CREDENTIALS
|
||||
from httpie.cli.constants import DEFAULT_FORMAT_OPTIONS, SEPARATOR_CREDENTIALS
|
||||
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
@ -181,3 +182,69 @@ def readable_file_arg(filename):
|
||||
return filename
|
||||
except IOError as ex:
|
||||
raise argparse.ArgumentTypeError(f'{filename}: {ex.args[1]}')
|
||||
|
||||
|
||||
def parse_format_options(s: str, defaults: Optional[dict]) -> dict:
|
||||
"""
|
||||
Parse `s` and update `defaults` with the parsed values.
|
||||
|
||||
>>> parse_format_options(
|
||||
... defaults={'json': {'indent': 4, 'sort_keys': True}},
|
||||
... s='json.indent:2,json.sort_keys:False',
|
||||
... )
|
||||
{'json': {'indent': 2, 'sort_keys': False}}
|
||||
|
||||
"""
|
||||
value_map = {
|
||||
'true': True,
|
||||
'false': False,
|
||||
}
|
||||
options = deepcopy(defaults or {})
|
||||
for option in s.split(','):
|
||||
try:
|
||||
path, value = option.lower().split(':')
|
||||
section, key = path.split('.')
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(f'invalid option {option!r}')
|
||||
|
||||
if value in value_map:
|
||||
parsed_value = value_map[value]
|
||||
else:
|
||||
if value.isnumeric():
|
||||
parsed_value = int(value)
|
||||
else:
|
||||
parsed_value = value
|
||||
|
||||
if defaults is None:
|
||||
options.setdefault(section, {})
|
||||
else:
|
||||
try:
|
||||
default_value = defaults[section][key]
|
||||
except KeyError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'invalid key {path!r}')
|
||||
|
||||
default_type, parsed_type = type(default_value), type(parsed_value)
|
||||
if parsed_type is not default_type:
|
||||
raise argparse.ArgumentTypeError(
|
||||
'invalid value'
|
||||
f' {value!r} in {option!r}'
|
||||
f' (expected {default_type.__name__}'
|
||||
f' got {parsed_type.__name__})'
|
||||
)
|
||||
|
||||
options[section][key] = parsed_value
|
||||
|
||||
return options
|
||||
|
||||
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
|
||||
s=','.join(DEFAULT_FORMAT_OPTIONS),
|
||||
defaults=None,
|
||||
)
|
||||
|
||||
|
||||
class UnsortedAction(argparse.Action):
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return 1
|
||||
|
@ -24,6 +24,7 @@ SEPARATOR_PROXY = ':'
|
||||
SEPARATOR_DATA_STRING = '='
|
||||
SEPARATOR_DATA_RAW_JSON = ':='
|
||||
SEPARATOR_FILE_UPLOAD = '@'
|
||||
SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEPARATOR_QUERY_PARAM = '=='
|
||||
@ -83,21 +84,22 @@ PRETTY_MAP = {
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
|
||||
DEFAULT_FORMAT_OPTIONS = [
|
||||
'headers.sort:true',
|
||||
'json.format:true',
|
||||
'json.indent:4',
|
||||
'json.sort_keys:true',
|
||||
]
|
||||
SORTED_FORMAT_OPTIONS = [
|
||||
'headers.sort:true',
|
||||
'json.sort_keys:true',
|
||||
]
|
||||
SORTED_FORMAT_OPTIONS_STRING = ','.join(SORTED_FORMAT_OPTIONS)
|
||||
UNSORTED_FORMAT_OPTIONS_STRING = ','.join(
|
||||
option.replace('true', 'false') for option in SORTED_FORMAT_OPTIONS)
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_BODY
|
||||
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||||
'ssl3': 'PROTOCOL_SSLv3',
|
||||
'tls1': 'PROTOCOL_TLSv1',
|
||||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||||
}
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
cli_arg: getattr(ssl, ssl_constant)
|
||||
for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
|
||||
if hasattr(ssl, ssl_constant)
|
||||
}
|
||||
|
@ -8,25 +8,29 @@ from textwrap import dedent, wrap
|
||||
from httpie import __doc__, __version__
|
||||
from httpie.cli.argparser import HTTPieArgumentParser
|
||||
from httpie.cli.argtypes import (
|
||||
KeyValueArgType, SessionNameValidator, readable_file_arg,
|
||||
KeyValueArgType, SessionNameValidator,
|
||||
readable_file_arg,
|
||||
)
|
||||
from httpie.cli.constants import (
|
||||
OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, SSL_VERSION_ARG_MAPPING,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, SORTED_FORMAT_OPTIONS_STRING,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING,
|
||||
)
|
||||
from httpie.output.formatters.colors import (
|
||||
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
|
||||
)
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
prog='http',
|
||||
description='%s <https://httpie.org>' % __doc__.strip(),
|
||||
epilog=dedent("""
|
||||
epilog=dedent('''
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
@ -34,7 +38,7 @@ parser = HTTPieArgumentParser(
|
||||
|
||||
https://github.com/jakubroztocil/httpie/issues
|
||||
|
||||
"""),
|
||||
'''),
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -43,18 +47,18 @@ parser = HTTPieArgumentParser(
|
||||
|
||||
positional = parser.add_argument_group(
|
||||
title='Positional Arguments',
|
||||
description=dedent("""
|
||||
description=dedent('''
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
|
||||
""")
|
||||
''')
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='method',
|
||||
metavar='METHOD',
|
||||
nargs=OPTIONAL,
|
||||
default=None,
|
||||
help="""
|
||||
help='''
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
This argument can be omitted in which case HTTPie will use POST if there
|
||||
@ -63,12 +67,12 @@ positional.add_argument(
|
||||
$ http example.org # => GET
|
||||
$ http example.org hello=world # => POST
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='url',
|
||||
metavar='URL',
|
||||
help="""
|
||||
help='''
|
||||
The scheme defaults to 'http://' if the URL does not include one.
|
||||
(You can override this with: --default-scheme=https)
|
||||
|
||||
@ -77,7 +81,7 @@ positional.add_argument(
|
||||
$ http :3000 # => http://localhost:3000
|
||||
$ http :/foo # => http://localhost/foo
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
positional.add_argument(
|
||||
dest='request_items',
|
||||
@ -85,7 +89,7 @@ positional.add_argument(
|
||||
nargs=ZERO_OR_MORE,
|
||||
default=None,
|
||||
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
||||
help=r"""
|
||||
help=r'''
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
@ -108,7 +112,8 @@ positional.add_argument(
|
||||
|
||||
'@' Form file fields (only with --form, -f):
|
||||
|
||||
cs@~/Documents/CV.pdf
|
||||
cv@~/Documents/CV.pdf
|
||||
cv@'~/Documents/CV.pdf;type=application/pdf'
|
||||
|
||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||
|
||||
@ -122,7 +127,7 @@ positional.add_argument(
|
||||
|
||||
field-name-with\:colon=value
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -137,24 +142,24 @@ content_type = parser.add_argument_group(
|
||||
content_type.add_argument(
|
||||
'--json', '-j',
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
(if not specified).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--form', '-f',
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||
specified). The presence of any file fields results in a
|
||||
multipart/form-data request.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -170,14 +175,14 @@ content_processing.add_argument(
|
||||
'--compress', '-x',
|
||||
action='count',
|
||||
default=0,
|
||||
help="""
|
||||
help='''
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
|
||||
Compression is skipped if it appears that compression ratio is
|
||||
negative. Compression can be forced by repeating the argument.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -191,12 +196,12 @@ output_processing.add_argument(
|
||||
dest='prettify',
|
||||
default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
help="""
|
||||
help='''
|
||||
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',
|
||||
@ -204,10 +209,10 @@ output_processing.add_argument(
|
||||
metavar='STYLE',
|
||||
default=DEFAULT_STYLE,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help="""
|
||||
Output coloring style (default is "{default}"). One of:
|
||||
help='''
|
||||
Output coloring style (default is "{default}"). It can be One of:
|
||||
|
||||
{available_styles}
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
|
||||
@ -215,15 +220,75 @@ output_processing.add_argument(
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
|
||||
""".format(
|
||||
'''.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
).strip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
)
|
||||
_sorted_kwargs = {
|
||||
'action': 'append_const',
|
||||
'const': SORTED_FORMAT_OPTIONS_STRING,
|
||||
'dest': 'format_options'
|
||||
}
|
||||
_unsorted_kwargs = {
|
||||
'action': 'append_const',
|
||||
'const': UNSORTED_FORMAT_OPTIONS_STRING,
|
||||
'dest': 'format_options'
|
||||
}
|
||||
# The closest approx. of the documented resetting to default via --no-<option>.
|
||||
# We hide them from the doc because they act only as low-level aliases here.
|
||||
output_processing.add_argument('--no-unsorted', **_sorted_kwargs, help=SUPPRESS)
|
||||
output_processing.add_argument('--no-sorted', **_unsorted_kwargs, help=SUPPRESS)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--unsorted',
|
||||
**_unsorted_kwargs,
|
||||
help=f'''
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
--format-options={UNSORTED_FORMAT_OPTIONS_STRING}
|
||||
|
||||
'''
|
||||
)
|
||||
output_processing.add_argument(
|
||||
'--sorted',
|
||||
**_sorted_kwargs,
|
||||
help=f'''
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
--format-options={SORTED_FORMAT_OPTIONS_STRING}
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
output_processing.add_argument(
|
||||
'--format-options',
|
||||
action='append',
|
||||
help='''
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) --pretty=all or --pretty=format.
|
||||
The following are the default options:
|
||||
|
||||
{option_list}
|
||||
|
||||
You may use this option multiple times, as well as specify multiple
|
||||
comma-separated options at the same time. For example, this modifies the
|
||||
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||
|
||||
--format-options json.sort_keys:false,json.indent:2
|
||||
|
||||
This is something you will typically put into your config file.
|
||||
|
||||
'''.format(
|
||||
option_list='\n'.join(
|
||||
(8 * ' ') + option for option in DEFAULT_FORMAT_OPTIONS).strip()
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output options
|
||||
@ -234,7 +299,7 @@ output_options.add_argument(
|
||||
'--print', '-p',
|
||||
dest='output_options',
|
||||
metavar='WHAT',
|
||||
help=f"""
|
||||
help=f'''
|
||||
String specifying what the output should contain:
|
||||
|
||||
'{OUT_REQ_HEAD}' request headers
|
||||
@ -247,69 +312,69 @@ output_options.add_argument(
|
||||
If the output is piped to another program or to a file, then only the
|
||||
response body is printed by default.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--headers', '-h',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_HEAD,
|
||||
help=f"""
|
||||
help=f'''
|
||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_BODY,
|
||||
help=f"""
|
||||
help=f'''
|
||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v',
|
||||
dest='verbose',
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Verbose output. Print the whole request as well as the response. Also print
|
||||
any intermediary requests/responses (such as redirects).
|
||||
It's a shortcut for: --all --print={0}
|
||||
|
||||
""".format(''.join(OUTPUT_OPTIONS))
|
||||
'''.format(''.join(OUTPUT_OPTIONS))
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
followed redirects (with --follow), the first unauthorized request when
|
||||
Digest auth is used (--auth=digest), etc.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--history-print', '-P',
|
||||
dest='output_options_history',
|
||||
metavar='WHAT',
|
||||
help="""
|
||||
help='''
|
||||
The same as --print, -p but applies only to intermediary requests/responses
|
||||
(such as redirects) when their inclusion is enabled with --all. If this
|
||||
options is not specified, then they are formatted the same way as the final
|
||||
response.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--stream', '-S',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Always stream the output by line, i.e., behave like `tail -f'.
|
||||
|
||||
Without --stream and with --pretty (either set or implied),
|
||||
@ -321,19 +386,19 @@ output_options.add_argument(
|
||||
It is useful also without --pretty: It ensures that the output is flushed
|
||||
more often and in smaller chunks.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--output', '-o',
|
||||
type=FileType('a+b'),
|
||||
dest='output_file',
|
||||
metavar='FILE',
|
||||
help="""
|
||||
help='''
|
||||
Save output to FILE instead of stdout. If --download is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
"""
|
||||
'''
|
||||
|
||||
)
|
||||
|
||||
@ -341,12 +406,12 @@ output_options.add_argument(
|
||||
'--download', '-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with --output
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
@ -354,11 +419,11 @@ output_options.add_argument(
|
||||
dest='download_resume',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Resume an interrupted download. Note that the --output option needs to be
|
||||
specified as well.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -376,7 +441,7 @@ sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help=f"""
|
||||
help=f'''
|
||||
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.
|
||||
@ -385,17 +450,17 @@ sessions.add_argument(
|
||||
|
||||
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help="""
|
||||
help='''
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -408,11 +473,11 @@ auth.add_argument(
|
||||
'--auth', '-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS]',
|
||||
help="""
|
||||
help='''
|
||||
If only the username is provided (-a username), HTTPie will prompt
|
||||
for the password.
|
||||
|
||||
""",
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
@ -431,12 +496,12 @@ auth.add_argument(
|
||||
'--auth-type', '-A',
|
||||
choices=_AuthTypeLazyChoices(),
|
||||
default=None,
|
||||
help="""
|
||||
help='''
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
|
||||
""".format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'''.format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
@ -456,10 +521,10 @@ auth.add_argument(
|
||||
'--ignore-netrc',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
""",
|
||||
''',
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -472,9 +537,9 @@ network.add_argument(
|
||||
'--offline',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Build the request and print it but don’t actually send it.
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--proxy',
|
||||
@ -482,43 +547,43 @@ network.add_argument(
|
||||
action='append',
|
||||
metavar='PROTOCOL:PROXY_URL',
|
||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||
help="""
|
||||
help='''
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||
and $HTTPS_proxy are supported as well.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--follow', '-F',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Follow 30x Location redirects.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-redirects',
|
||||
type=int,
|
||||
default=30,
|
||||
help="""
|
||||
help='''
|
||||
By default, requests have a limit of 30 redirects (works with --follow).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-headers',
|
||||
type=int,
|
||||
default=0,
|
||||
help="""
|
||||
help='''
|
||||
The maximum number of response headers to be read before giving up
|
||||
(default 0, i.e., no limit).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
@ -526,7 +591,7 @@ network.add_argument(
|
||||
type=float,
|
||||
default=0,
|
||||
metavar='SECONDS',
|
||||
help="""
|
||||
help='''
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
This is not a time limit on the entire response download;
|
||||
@ -534,13 +599,13 @@ network.add_argument(
|
||||
timeout seconds (more precisely, if no bytes have been received on
|
||||
the underlying socket for timeout seconds).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--check-status',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
@ -550,16 +615,16 @@ network.add_argument(
|
||||
3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--path-as-is',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -570,47 +635,58 @@ ssl = parser.add_argument_group(title='SSL')
|
||||
ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
help="""
|
||||
help='''
|
||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||
variable instead.)
|
||||
"""
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
|
||||
dest='ssl_version',
|
||||
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
|
||||
help="""
|
||||
choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())),
|
||||
help='''
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. Available protocols
|
||||
may vary depending on OpenSSL installation (only the supported ones
|
||||
are shown here).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ciphers',
|
||||
help=f'''
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
is used:
|
||||
|
||||
{DEFAULT_SSL_CIPHERS}
|
||||
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--cert',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
help='''
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
specify --cert-key separately.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
ssl.add_argument(
|
||||
'--cert-key',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
help='''
|
||||
The private key to use with SSL. Only needed if --cert is given and the
|
||||
certificate file does not contain the private key.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -623,53 +699,53 @@ troubleshooting.add_argument(
|
||||
'--ignore-stdin', '-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Do not attempt to read stdin.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help',
|
||||
default=SUPPRESS,
|
||||
help="""
|
||||
help='''
|
||||
Show this help message and exit.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help="""
|
||||
help='''
|
||||
Show version and exit.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--default-scheme',
|
||||
default="http",
|
||||
help="""
|
||||
help='''
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
@ -6,7 +6,8 @@ from httpie.cli.argtypes import KeyValueArg
|
||||
from httpie.cli.constants import (
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY, SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
)
|
||||
from httpie.cli.dicts import (
|
||||
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict,
|
||||
@ -95,7 +96,10 @@ def process_query_param_arg(arg: KeyValueArg) -> str:
|
||||
|
||||
|
||||
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
filename = arg.value
|
||||
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
||||
filename = parts[0]
|
||||
mime_type = parts[1] if len(parts) > 1 else None
|
||||
|
||||
try:
|
||||
with open(os.path.expanduser(filename), 'rb') as f:
|
||||
contents = f.read()
|
||||
@ -104,7 +108,7 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
return (
|
||||
os.path.basename(filename),
|
||||
BytesIO(contents),
|
||||
get_content_type(filename),
|
||||
mime_type or get_content_type(filename),
|
||||
)
|
||||
|
||||
|
||||
|
@ -9,24 +9,17 @@ from typing import Iterable, Union
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
|
||||
from httpie import __version__
|
||||
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
|
||||
from httpie.cli.dicts import RequestHeadersDict
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.sessions import get_httpie_session
|
||||
from httpie.utils import repr_dict
|
||||
|
||||
|
||||
try:
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
# <https://urllib3.readthedocs.io/en/latest/security.html>
|
||||
urllib3.disable_warnings()
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
|
||||
from httpie.utils import get_expired_cookies, repr_dict
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
@ -57,6 +50,8 @@ def collect_messages(
|
||||
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
|
||||
requests_session = build_requests_session(
|
||||
ssl_version=args.ssl_version,
|
||||
ciphers=args.ciphers,
|
||||
verify=bool(send_kwargs_mergeable_from_env['verify'])
|
||||
)
|
||||
|
||||
if httpie_session:
|
||||
@ -86,6 +81,7 @@ def collect_messages(
|
||||
if args.compress and prepared_request.body:
|
||||
compress_body(prepared_request, always=args.compress > 1)
|
||||
response_count = 0
|
||||
expired_cookies = []
|
||||
while prepared_request:
|
||||
yield prepared_request
|
||||
if not args.offline:
|
||||
@ -99,6 +95,12 @@ def collect_messages(
|
||||
**send_kwargs_merged,
|
||||
**send_kwargs,
|
||||
)
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
expired_cookies += get_expired_cookies(
|
||||
headers=response.raw._original_response.msg._headers
|
||||
)
|
||||
|
||||
response_count += 1
|
||||
if response.next:
|
||||
if args.max_redirects and response_count == args.max_redirects:
|
||||
@ -114,6 +116,10 @@ def collect_messages(
|
||||
if httpie_session:
|
||||
if httpie_session.is_new() or not args.session_read_only:
|
||||
httpie_session.cookies = requests_session.cookies
|
||||
httpie_session.remove_cookies(
|
||||
# TODO: take path & domain into account?
|
||||
cookie['name'] for cookie in expired_cookies
|
||||
)
|
||||
httpie_session.save()
|
||||
|
||||
|
||||
@ -121,6 +127,7 @@ def collect_messages(
|
||||
@contextmanager
|
||||
def max_headers(limit):
|
||||
# <https://github.com/jakubroztocil/httpie/issues/802>
|
||||
# noinspection PyUnresolvedReferences
|
||||
orig = http.client._MAXHEADERS
|
||||
http.client._MAXHEADERS = limit or float('Inf')
|
||||
try:
|
||||
@ -145,29 +152,23 @@ def compress_body(request: requests.PreparedRequest, always: bool):
|
||||
request.headers['Content-Length'] = str(len(deflated_data))
|
||||
|
||||
|
||||
class HTTPieHTTPSAdapter(HTTPAdapter):
|
||||
|
||||
def __init__(self, ssl_version=None, **kwargs):
|
||||
self._ssl_version = ssl_version
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs['ssl_version'] = self._ssl_version
|
||||
super().init_poolmanager(*args, **kwargs)
|
||||
|
||||
|
||||
def build_requests_session(
|
||||
verify: bool,
|
||||
ssl_version: str = None,
|
||||
ciphers: str = None,
|
||||
) -> requests.Session:
|
||||
requests_session = requests.Session()
|
||||
|
||||
# Install our adapter.
|
||||
requests_session.mount('https://', HTTPieHTTPSAdapter(
|
||||
https_adapter = HTTPieHTTPSAdapter(
|
||||
ciphers=ciphers,
|
||||
verify=verify,
|
||||
ssl_version=(
|
||||
SSL_VERSION_ARG_MAPPING[ssl_version]
|
||||
AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
|
||||
if ssl_version else None
|
||||
)
|
||||
))
|
||||
),
|
||||
)
|
||||
requests_session.mount('https://', https_adapter)
|
||||
|
||||
# Install adapters from plugins.
|
||||
for plugin_cls in plugin_manager.get_transport_plugins():
|
||||
|
@ -8,11 +8,54 @@ from httpie import __version__
|
||||
from httpie.compat import is_windows
|
||||
|
||||
|
||||
DEFAULT_CONFIG_DIR = Path(os.environ.get(
|
||||
'HTTPIE_CONFIG_DIR',
|
||||
os.path.expanduser('~/.httpie') if not is_windows else
|
||||
os.path.expandvars(r'%APPDATA%\\httpie')
|
||||
))
|
||||
ENV_XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
|
||||
ENV_HTTPIE_CONFIG_DIR = 'HTTPIE_CONFIG_DIR'
|
||||
DEFAULT_CONFIG_DIRNAME = 'httpie'
|
||||
DEFAULT_RELATIVE_XDG_CONFIG_HOME = Path('.config')
|
||||
DEFAULT_RELATIVE_LEGACY_CONFIG_DIR = Path('.httpie')
|
||||
DEFAULT_WINDOWS_CONFIG_DIR = Path(
|
||||
os.path.expandvars('%APPDATA%')) / DEFAULT_CONFIG_DIRNAME
|
||||
|
||||
|
||||
def get_default_config_dir() -> Path:
|
||||
"""
|
||||
Return the path to the httpie configuration directory.
|
||||
|
||||
This directory isn't guaranteed to exist, and nor are any of its
|
||||
ancestors (only the legacy ~/.httpie, if returned, is guaranteed to exist).
|
||||
|
||||
XDG Base Directory Specification support:
|
||||
|
||||
<https://wiki.archlinux.org/index.php/XDG_Base_Directory>
|
||||
|
||||
$XDG_CONFIG_HOME is supported; $XDG_CONFIG_DIRS is not
|
||||
|
||||
"""
|
||||
# 1. explicitly set through env
|
||||
env_config_dir = os.environ.get(ENV_HTTPIE_CONFIG_DIR)
|
||||
if env_config_dir:
|
||||
return Path(env_config_dir)
|
||||
|
||||
# 2. Windows
|
||||
if is_windows:
|
||||
return DEFAULT_WINDOWS_CONFIG_DIR
|
||||
|
||||
home_dir = Path.home()
|
||||
|
||||
# 3. legacy ~/.httpie
|
||||
legacy_config_dir = home_dir / DEFAULT_RELATIVE_LEGACY_CONFIG_DIR
|
||||
if legacy_config_dir.exists():
|
||||
return legacy_config_dir
|
||||
|
||||
# 4. XDG
|
||||
xdg_config_home_dir = os.environ.get(
|
||||
ENV_XDG_CONFIG_HOME, # 4.1. explicit
|
||||
home_dir / DEFAULT_RELATIVE_XDG_CONFIG_HOME # 4.2. default
|
||||
)
|
||||
return Path(xdg_config_home_dir) / DEFAULT_CONFIG_DIRNAME
|
||||
|
||||
|
||||
DEFAULT_CONFIG_DIR = get_default_config_dir()
|
||||
|
||||
|
||||
class ConfigFileError(Exception):
|
||||
|
@ -13,10 +13,11 @@ from httpie.client import collect_messages
|
||||
from httpie.context import Environment
|
||||
from httpie.downloads import Downloader
|
||||
from httpie.output.writer import write_message, write_stream
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.status import ExitStatus, http_status_to_exit_status
|
||||
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def main(
|
||||
args: List[Union[str, bytes]] = sys.argv,
|
||||
env=Environment(),
|
||||
|
@ -3,6 +3,10 @@ from httpie.plugins import FormatterPlugin
|
||||
|
||||
class HeadersFormatter(FormatterPlugin):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['headers']['sort']
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
"""
|
||||
Sorts headers by name while retaining relative
|
||||
|
@ -4,11 +4,12 @@ import json
|
||||
from httpie.plugins import FormatterPlugin
|
||||
|
||||
|
||||
DEFAULT_INDENT = 4
|
||||
|
||||
|
||||
class JSONFormatter(FormatterPlugin):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['json']['format']
|
||||
|
||||
def format_body(self, body: str, mime: str) -> str:
|
||||
maybe_json = [
|
||||
'json',
|
||||
@ -26,8 +27,8 @@ class JSONFormatter(FormatterPlugin):
|
||||
# unicode escapes to improve readability.
|
||||
body = json.dumps(
|
||||
obj=obj,
|
||||
sort_keys=True,
|
||||
sort_keys=self.format_options['json']['sort_keys'],
|
||||
ensure_ascii=False,
|
||||
indent=DEFAULT_INDENT
|
||||
indent=self.format_options['json']['indent']
|
||||
)
|
||||
return body
|
||||
|
@ -1,7 +1,8 @@
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
from httpie.plugins import plugin_manager, ConverterPlugin
|
||||
from httpie.plugins import ConverterPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.context import Environment
|
||||
|
||||
|
||||
|
@ -152,6 +152,7 @@ def get_stream_type_and_kwargs(
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
format_options=args.format_options,
|
||||
)
|
||||
}
|
||||
else:
|
||||
|
@ -7,18 +7,3 @@ from httpie.plugins.base import (
|
||||
AuthPlugin, FormatterPlugin,
|
||||
ConverterPlugin, TransportPlugin
|
||||
)
|
||||
from httpie.plugins.manager import PluginManager
|
||||
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.colors import ColorFormatter
|
||||
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager.register(
|
||||
BasicAuthPlugin,
|
||||
DigestAuthPlugin,
|
||||
HeadersFormatter,
|
||||
JSONFormatter,
|
||||
ColorFormatter,
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ class BasePlugin:
|
||||
# The name of the plugin, eg. "My auth".
|
||||
name = None
|
||||
|
||||
# Optional short description. Will be be shown in the help
|
||||
# Optional short description. It will be shown in the help
|
||||
# under --auth-type.
|
||||
description = None
|
||||
|
||||
@ -15,7 +15,9 @@ class AuthPlugin(BasePlugin):
|
||||
"""
|
||||
Base auth plugin class.
|
||||
|
||||
See <https://github.com/httpie/httpie-ntlm> for an example auth plugin.
|
||||
See httpie-ntlm for an example auth plugin:
|
||||
|
||||
<https://github.com/httpie/httpie-ntlm>
|
||||
|
||||
See also `test_auth_plugins.py`
|
||||
|
||||
@ -33,13 +35,22 @@ class AuthPlugin(BasePlugin):
|
||||
# Set this to `False` to disable the parsing and error handling.
|
||||
auth_parse = True
|
||||
|
||||
# Set to `True` to make it possible for this auth
|
||||
# plugin to acquire credentials from the user’s netrc file(s).
|
||||
# It is used as a fallback when the credentials are not provided explicitly
|
||||
# through `--auth, -a`. Enabling this will allow skipping `--auth, -a`
|
||||
# even when `auth_require` is set `True` (provided that netrc provides
|
||||
# credential for a given host).
|
||||
netrc_parse = False
|
||||
|
||||
# If both `auth_parse` and `prompt_password` are set to `True`,
|
||||
# and the value of `-a` lacks the password part,
|
||||
# then the user will be prompted to type the password in.
|
||||
prompt_password = True
|
||||
|
||||
# Will be set to the raw value of `-a` (if provided) before
|
||||
# `get_auth()` gets called.
|
||||
# `get_auth()` gets called. If the credentials came from a netrc file,
|
||||
# then this is `None`.
|
||||
raw_auth = None
|
||||
|
||||
def get_auth(self, username=None, password=None):
|
||||
@ -58,8 +69,13 @@ class AuthPlugin(BasePlugin):
|
||||
|
||||
class TransportPlugin(BasePlugin):
|
||||
"""
|
||||
Requests transport adapter docs:
|
||||
|
||||
https://2.python-requests.org/en/latest/user/advanced/#transport-adapters
|
||||
<https://requests.readthedocs.io/en/latest/user/advanced/#transport-adapters>
|
||||
|
||||
See httpie-unixsocket for an example transport plugin:
|
||||
|
||||
<https://github.com/httpie/httpie-unixsocket>
|
||||
|
||||
"""
|
||||
|
||||
@ -76,6 +92,14 @@ class TransportPlugin(BasePlugin):
|
||||
|
||||
|
||||
class ConverterPlugin(BasePlugin):
|
||||
"""
|
||||
Possibly converts response data for prettified terminal display.
|
||||
|
||||
See httpie-msgpack for an example converter plugin:
|
||||
|
||||
<https://github.com/rasky/httpie-msgpack>.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, mime):
|
||||
self.mime = mime
|
||||
@ -89,17 +113,22 @@ class ConverterPlugin(BasePlugin):
|
||||
|
||||
|
||||
class FormatterPlugin(BasePlugin):
|
||||
"""
|
||||
Possibly formats response body & headers for prettified terminal display.
|
||||
|
||||
"""
|
||||
group_name = 'format'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
:param env: an class:`Environment` instance
|
||||
:param kwargs: additional keyword argument that some
|
||||
processor might require.
|
||||
formatters might require.
|
||||
|
||||
"""
|
||||
self.enabled = True
|
||||
self.kwargs = kwargs
|
||||
self.format_options = kwargs['format_options']
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
"""Return processed `headers`
|
||||
|
@ -22,6 +22,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
|
||||
See https://github.com/jakubroztocil/httpie/issues/212
|
||||
|
||||
"""
|
||||
# noinspection PyTypeChecker
|
||||
request.headers['Authorization'] = type(self).make_header(
|
||||
self.username, self.password).encode('latin1')
|
||||
return request
|
||||
@ -36,6 +37,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
|
||||
class BasicAuthPlugin(BuiltinAuthPlugin):
|
||||
name = 'Basic HTTP auth'
|
||||
auth_type = 'basic'
|
||||
netrc_parse = True
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def get_auth(self, username: str, password: str) -> HTTPBasicAuth:
|
||||
@ -43,9 +45,9 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
|
||||
|
||||
|
||||
class DigestAuthPlugin(BuiltinAuthPlugin):
|
||||
|
||||
name = 'Digest HTTP auth'
|
||||
auth_type = 'digest'
|
||||
netrc_parse = True
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def get_auth(
|
||||
|
18
httpie/plugins/registry.py
Normal file
18
httpie/plugins/registry.py
Normal file
@ -0,0 +1,18 @@
|
||||
from httpie.plugins.manager import PluginManager
|
||||
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.colors import ColorFormatter
|
||||
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
|
||||
|
||||
# Register all built-in plugins.
|
||||
plugin_manager.register(
|
||||
BasicAuthPlugin,
|
||||
DigestAuthPlugin,
|
||||
HeadersFormatter,
|
||||
JSONFormatter,
|
||||
ColorFormatter,
|
||||
)
|
@ -5,7 +5,7 @@ Persistent, JSON-serialized sessions.
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from typing import Iterable, Optional, Union
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from requests.auth import AuthBase
|
||||
@ -13,7 +13,7 @@ from requests.cookies import RequestsCookieJar, create_cookie
|
||||
|
||||
from httpie.cli.dicts import RequestHeadersDict
|
||||
from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
|
||||
|
||||
SESSIONS_DIR_NAME = 'sessions'
|
||||
@ -144,3 +144,8 @@ class Session(BaseConfigDict):
|
||||
def auth(self, auth: dict):
|
||||
assert {'type', 'raw_auth'} == auth.keys()
|
||||
self['auth'] = auth
|
||||
|
||||
def remove_cookies(self, names: Iterable[str]):
|
||||
for name in names:
|
||||
if name in self['cookies']:
|
||||
del self['cookies'][name]
|
||||
|
63
httpie/ssl.py
Normal file
63
httpie/ssl.py
Normal file
@ -0,0 +1,63 @@
|
||||
import ssl
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
# noinspection PyPackageRequirements
|
||||
from urllib3.util.ssl_ import (
|
||||
DEFAULT_CIPHERS, create_urllib3_context,
|
||||
resolve_ssl_version,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_SSL_CIPHERS = DEFAULT_CIPHERS
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||||
'ssl3': 'PROTOCOL_SSLv3',
|
||||
'tls1': 'PROTOCOL_TLSv1',
|
||||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||||
}
|
||||
AVAILABLE_SSL_VERSION_ARG_MAPPING = {
|
||||
arg: getattr(ssl, constant_name)
|
||||
for arg, constant_name in SSL_VERSION_ARG_MAPPING.items()
|
||||
if hasattr(ssl, constant_name)
|
||||
}
|
||||
|
||||
|
||||
class HTTPieHTTPSAdapter(HTTPAdapter):
|
||||
def __init__(
|
||||
self,
|
||||
verify: bool,
|
||||
ssl_version: str = None,
|
||||
ciphers: str = None,
|
||||
**kwargs
|
||||
):
|
||||
self._ssl_context = self._create_ssl_context(
|
||||
verify=verify,
|
||||
ssl_version=ssl_version,
|
||||
ciphers=ciphers,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs['ssl_context'] = self._ssl_context
|
||||
return super().init_poolmanager(*args, **kwargs)
|
||||
|
||||
def proxy_manager_for(self, *args, **kwargs):
|
||||
kwargs['ssl_context'] = self._ssl_context
|
||||
return super().proxy_manager_for(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _create_ssl_context(
|
||||
verify: bool,
|
||||
ssl_version: str = None,
|
||||
ciphers: str = None,
|
||||
) -> ssl.SSLContext:
|
||||
return create_urllib3_context(
|
||||
ciphers=ciphers,
|
||||
ssl_version=resolve_ssl_version(ssl_version),
|
||||
# Since we are using a custom SSL context, we need to pass this
|
||||
# here manually, even though it’s also passed to the connection
|
||||
# in `super().cert_verify()`.
|
||||
cert_reqs=ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
|
||||
)
|
@ -1,8 +1,12 @@
|
||||
from __future__ import division
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from http.cookiejar import parse_ns_headers
|
||||
from pprint import pformat
|
||||
from typing import List, Tuple
|
||||
|
||||
import requests.auth
|
||||
|
||||
@ -83,3 +87,27 @@ def get_content_type(filename):
|
||||
if encoding:
|
||||
content_type = '%s; charset=%s' % (mime, encoding)
|
||||
return content_type
|
||||
|
||||
|
||||
def get_expired_cookies(
|
||||
headers: List[Tuple[str, str]],
|
||||
now: float = None
|
||||
) -> List[dict]:
|
||||
now = now or time.time()
|
||||
attr_sets: List[Tuple[str, str]] = parse_ns_headers(
|
||||
value for name, value in headers
|
||||
if name.lower() == 'set-cookie'
|
||||
)
|
||||
cookies = [
|
||||
# The first attr name is the cookie name.
|
||||
dict(attrs[1:], name=attrs[0][0])
|
||||
for attrs in attr_sets
|
||||
]
|
||||
return [
|
||||
{
|
||||
'name': cookie['name'],
|
||||
'path': cookie.get('path', '/')
|
||||
}
|
||||
for cookie in cookies
|
||||
if cookie.get('expires', float('Inf')) <= now
|
||||
]
|
||||
|
@ -4,6 +4,7 @@
|
||||
[tool:pytest]
|
||||
# <https://docs.pytest.org/en/latest/customize.html>
|
||||
norecursedirs = tests/fixtures
|
||||
addopts = --tb=native
|
||||
|
||||
|
||||
[pycodestyle]
|
||||
|
@ -3,6 +3,7 @@ import mock
|
||||
import pytest
|
||||
|
||||
from httpie.plugins.builtin import HTTPBasicAuth
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.utils import ExplicitNullAuth
|
||||
from utils import http, add_auth, HTTP_OK, MockEnvironment
|
||||
import httpie.cli.constants
|
||||
@ -78,6 +79,8 @@ def test_missing_auth(httpbin):
|
||||
|
||||
|
||||
def test_netrc(httpbin_both):
|
||||
# This one gets handled by requests (no --auth, --auth-type present),
|
||||
# that’s why we patch inside `requests.sessions`.
|
||||
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
|
||||
get_netrc_auth.return_value = ('httpie', 'password')
|
||||
r = http(httpbin_both + '/basic-auth/httpie/password')
|
||||
@ -86,24 +89,54 @@ def test_netrc(httpbin_both):
|
||||
|
||||
|
||||
def test_ignore_netrc(httpbin_both):
|
||||
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
|
||||
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
|
||||
get_netrc_auth.return_value = ('httpie', 'password')
|
||||
r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password')
|
||||
assert get_netrc_auth.call_count == 0
|
||||
assert 'HTTP/1.1 401 UNAUTHORIZED' in r
|
||||
|
||||
|
||||
def test_ignore_netrc_null_auth():
|
||||
args = httpie.cli.definition.parser.parse_args(
|
||||
args=['--ignore-netrc', 'example.org'],
|
||||
env=MockEnvironment(),
|
||||
)
|
||||
assert isinstance(args.auth, ExplicitNullAuth)
|
||||
|
||||
|
||||
def test_ignore_netrc_together_with_auth():
|
||||
args = httpie.cli.definition.parser.parse_args(
|
||||
args=['--ignore-netrc', '--auth=username:password', 'example.org'],
|
||||
env=MockEnvironment(),
|
||||
)
|
||||
assert isinstance(args.auth, HTTPBasicAuth)
|
||||
|
||||
|
||||
def test_ignore_netrc_with_auth_type_resulting_in_missing_auth(httpbin):
|
||||
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
|
||||
get_netrc_auth.return_value = ('httpie', 'password')
|
||||
r = http(
|
||||
'--ignore-netrc',
|
||||
'--auth-type=basic',
|
||||
httpbin + '/basic-auth/httpie/password',
|
||||
tolerate_error_exit_status=True,
|
||||
)
|
||||
assert get_netrc_auth.call_count == 0
|
||||
assert r.exit_status == ExitStatus.ERROR
|
||||
assert '--auth required' in r.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['auth_type', 'endpoint'],
|
||||
argvalues=[
|
||||
('basic', '/basic-auth/httpie/password'),
|
||||
('digest', '/digest-auth/auth/httpie/password'),
|
||||
],
|
||||
)
|
||||
def test_auth_plugin_netrc_parse(auth_type, endpoint, httpbin):
|
||||
# Test
|
||||
with mock.patch('httpie.cli.argparser.get_netrc_auth') as get_netrc_auth:
|
||||
get_netrc_auth.return_value = ('httpie', 'password')
|
||||
r = http('--auth-type', auth_type, httpbin + endpoint)
|
||||
assert get_netrc_auth.call_count == 1
|
||||
assert HTTP_OK in r
|
||||
|
||||
|
||||
def test_ignore_netrc_null_auth():
|
||||
args = httpie.cli.definition.parser.parse_args(
|
||||
args=['--ignore-netrc', 'example.org'],
|
||||
env=MockEnvironment(),
|
||||
)
|
||||
assert isinstance(args.auth, ExplicitNullAuth)
|
||||
|
@ -1,9 +1,11 @@
|
||||
from mock import mock
|
||||
|
||||
from httpie.cli.constants import SEPARATOR_CREDENTIALS
|
||||
from httpie.plugins import AuthPlugin, plugin_manager
|
||||
from httpie.plugins import AuthPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from utils import http, HTTP_OK
|
||||
|
||||
|
||||
# TODO: run all these tests in session mode as well
|
||||
|
||||
USERNAME = 'user'
|
||||
|
@ -1,7 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.config import Config
|
||||
from httpie.config import (
|
||||
Config, DEFAULT_CONFIG_DIRNAME, DEFAULT_RELATIVE_LEGACY_CONFIG_DIR,
|
||||
DEFAULT_RELATIVE_XDG_CONFIG_HOME, DEFAULT_WINDOWS_CONFIG_DIR,
|
||||
ENV_HTTPIE_CONFIG_DIR, ENV_XDG_CONFIG_HOME, get_default_config_dir,
|
||||
)
|
||||
from utils import HTTP_OK, MockEnvironment, http
|
||||
|
||||
|
||||
@ -48,3 +55,50 @@ def test_default_options_overwrite(httpbin):
|
||||
assert r.json['json'] == {
|
||||
"foo": "bar"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_windows, reason='XDG_CONFIG_HOME needs *nix')
|
||||
def test_explicit_xdg_config_home(monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||
home_dir = tmp_path
|
||||
monkeypatch.delenv(ENV_HTTPIE_CONFIG_DIR, raising=False)
|
||||
monkeypatch.setenv('HOME', str(home_dir))
|
||||
custom_xdg_config_home = home_dir / 'custom_xdg_config_home'
|
||||
monkeypatch.setenv(ENV_XDG_CONFIG_HOME, str(custom_xdg_config_home))
|
||||
expected_config_dir = custom_xdg_config_home / DEFAULT_CONFIG_DIRNAME
|
||||
assert get_default_config_dir() == expected_config_dir
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_windows, reason='XDG_CONFIG_HOME needs *nix')
|
||||
def test_default_xdg_config_home(monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||
home_dir = tmp_path
|
||||
monkeypatch.delenv(ENV_HTTPIE_CONFIG_DIR, raising=False)
|
||||
monkeypatch.delenv(ENV_XDG_CONFIG_HOME, raising=False)
|
||||
monkeypatch.setenv('HOME', str(home_dir))
|
||||
expected_config_dir = (
|
||||
home_dir
|
||||
/ DEFAULT_RELATIVE_XDG_CONFIG_HOME
|
||||
/ DEFAULT_CONFIG_DIRNAME
|
||||
)
|
||||
assert get_default_config_dir() == expected_config_dir
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_windows, reason='legacy config dir needs *nix')
|
||||
def test_legacy_config_dir(monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||
home_dir = tmp_path
|
||||
monkeypatch.delenv(ENV_HTTPIE_CONFIG_DIR, raising=False)
|
||||
monkeypatch.setenv('HOME', str(home_dir))
|
||||
legacy_config_dir = home_dir / DEFAULT_RELATIVE_LEGACY_CONFIG_DIR
|
||||
legacy_config_dir.mkdir()
|
||||
assert get_default_config_dir() == legacy_config_dir
|
||||
|
||||
|
||||
def test_custom_config_dir(monkeypatch: MonkeyPatch, tmp_path: Path):
|
||||
httpie_config_dir = tmp_path / 'custom/directory'
|
||||
monkeypatch.setenv(ENV_HTTPIE_CONFIG_DIR, str(httpie_config_dir))
|
||||
assert get_default_config_dir() == httpie_config_dir
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_windows, reason='windows-only')
|
||||
def test_windows_config_dir(monkeypatch: MonkeyPatch):
|
||||
monkeypatch.delenv(ENV_HTTPIE_CONFIG_DIR, raising=False)
|
||||
assert get_default_config_dir() == DEFAULT_WINDOWS_CONFIG_DIR
|
||||
|
@ -1,12 +1,19 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from tempfile import gettempdir
|
||||
from urllib.request import urlopen
|
||||
|
||||
import pytest
|
||||
|
||||
from utils import MockEnvironment, http, HTTP_OK, COLOR, CRLF
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.cli.argtypes import (
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
parse_format_options,
|
||||
)
|
||||
from httpie.cli.definition import parser
|
||||
from httpie.output.formatters.colors import get_lexer
|
||||
from httpie.status import ExitStatus
|
||||
from utils import COLOR, CRLF, HTTP_OK, MockEnvironment, http
|
||||
|
||||
|
||||
@pytest.mark.parametrize('stdout_isatty', [True, False])
|
||||
@ -58,19 +65,19 @@ class TestColors:
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
|
||||
argvalues=[
|
||||
('application/json', False, None, 'JSON'),
|
||||
('application/json', False, None, 'JSON'),
|
||||
('application/json+foo', False, None, 'JSON'),
|
||||
('application/foo+json', False, None, 'JSON'),
|
||||
('application/json-foo', False, None, 'JSON'),
|
||||
('application/x-json', False, None, 'JSON'),
|
||||
('foo/json', False, None, 'JSON'),
|
||||
('foo/json+bar', False, None, 'JSON'),
|
||||
('foo/bar+json', False, None, 'JSON'),
|
||||
('foo/json-foo', False, None, 'JSON'),
|
||||
('foo/x-json', False, None, 'JSON'),
|
||||
('application/x-json', False, None, 'JSON'),
|
||||
('foo/json', False, None, 'JSON'),
|
||||
('foo/json+bar', False, None, 'JSON'),
|
||||
('foo/bar+json', False, None, 'JSON'),
|
||||
('foo/json-foo', False, None, 'JSON'),
|
||||
('foo/x-json', False, None, 'JSON'),
|
||||
('application/vnd.comverge.grid+hal+json', False, None, 'JSON'),
|
||||
('text/plain', True, '{}', 'JSON'),
|
||||
('text/plain', True, 'foo', 'Text only'),
|
||||
('text/plain', True, '{}', 'JSON'),
|
||||
('text/plain', True, 'foo', 'Text only'),
|
||||
]
|
||||
)
|
||||
def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
|
||||
@ -83,7 +90,7 @@ class TestColors:
|
||||
|
||||
|
||||
class TestPrettyOptions:
|
||||
"""Test the --pretty flag handling."""
|
||||
"""Test the --pretty handling."""
|
||||
|
||||
def test_pretty_enabled_by_default(self, httpbin):
|
||||
env = MockEnvironment(colors=256)
|
||||
@ -138,6 +145,7 @@ class TestLineEndings:
|
||||
and as the headers/body separator.
|
||||
|
||||
"""
|
||||
|
||||
def _validate_crlf(self, msg):
|
||||
lines = iter(msg.splitlines(True))
|
||||
for header in lines:
|
||||
@ -171,3 +179,199 @@ class TestLineEndings:
|
||||
def test_CRLF_formatted_request(self, httpbin):
|
||||
r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get')
|
||||
self._validate_crlf(r)
|
||||
|
||||
|
||||
class TestFormatOptions:
|
||||
def test_header_formatting_options(self):
|
||||
def get_headers(sort):
|
||||
return http(
|
||||
'--offline', '--print=H',
|
||||
'--format-options', 'headers.sort:' + sort,
|
||||
'example.org', 'ZZZ:foo', 'XXX:foo',
|
||||
)
|
||||
|
||||
r_sorted = get_headers('true')
|
||||
r_unsorted = get_headers('false')
|
||||
assert r_sorted != r_unsorted
|
||||
assert f'XXX: foo{CRLF}ZZZ: foo' in r_sorted
|
||||
assert f'ZZZ: foo{CRLF}XXX: foo' in r_unsorted
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['options', 'expected_json'],
|
||||
argvalues=[
|
||||
# @formatter:off
|
||||
(
|
||||
'json.sort_keys:true,json.indent:4',
|
||||
json.dumps({'a': 0, 'b': 0}, indent=4),
|
||||
),
|
||||
(
|
||||
'json.sort_keys:false,json.indent:2',
|
||||
json.dumps({'b': 0, 'a': 0}, indent=2),
|
||||
),
|
||||
(
|
||||
'json.format:false',
|
||||
json.dumps({'b': 0, 'a': 0}),
|
||||
),
|
||||
# @formatter:on
|
||||
]
|
||||
)
|
||||
def test_json_formatting_options(self, options: str, expected_json: str):
|
||||
r = http(
|
||||
'--offline', '--print=B',
|
||||
'--format-options', options,
|
||||
'example.org', 'b:=0', 'a:=0',
|
||||
)
|
||||
assert expected_json in r
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['defaults', 'options_string', 'expected'],
|
||||
argvalues=[
|
||||
# @formatter:off
|
||||
({'foo': {'bar': 1}}, 'foo.bar:2', {'foo': {'bar': 2}}),
|
||||
({'foo': {'bar': True}}, 'foo.bar:false', {'foo': {'bar': False}}),
|
||||
({'foo': {'bar': 'a'}}, 'foo.bar:b', {'foo': {'bar': 'b'}}),
|
||||
# @formatter:on
|
||||
]
|
||||
)
|
||||
def test_parse_format_options(self, defaults, options_string, expected):
|
||||
actual = parse_format_options(s=options_string, defaults=defaults)
|
||||
assert expected == actual
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['options_string', 'expected_error'],
|
||||
argvalues=[
|
||||
('foo:2', 'invalid option'),
|
||||
('foo.baz:2', 'invalid key'),
|
||||
('foo.bar:false', 'expected int got bool'),
|
||||
]
|
||||
)
|
||||
def test_parse_format_options_errors(self, options_string, expected_error):
|
||||
defaults = {
|
||||
'foo': {
|
||||
'bar': 1
|
||||
}
|
||||
}
|
||||
with pytest.raises(argparse.ArgumentTypeError, match=expected_error):
|
||||
parse_format_options(s=options_string, defaults=defaults)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['args', 'expected_format_options'],
|
||||
argvalues=[
|
||||
(
|
||||
[
|
||||
'--format-options',
|
||||
'headers.sort:false,json.sort_keys:false',
|
||||
'--format-options=json.indent:10'
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': False
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': False,
|
||||
'indent': 10,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
(
|
||||
[
|
||||
'--unsorted'
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': False
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': False,
|
||||
'indent': 4,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
(
|
||||
[
|
||||
'--format-options=headers.sort:true',
|
||||
'--unsorted',
|
||||
'--format-options=headers.sort:true',
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': True
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': False,
|
||||
'indent': 4,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
(
|
||||
[
|
||||
'--no-format-options', # --no-<option> anywhere resets
|
||||
'--format-options=headers.sort:true',
|
||||
'--unsorted',
|
||||
'--format-options=headers.sort:true',
|
||||
],
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
),
|
||||
(
|
||||
[
|
||||
'--format-options=json.indent:2',
|
||||
'--unsorted',
|
||||
'--no-unsorted',
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': True
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': True,
|
||||
'indent': 2,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
(
|
||||
[
|
||||
'--format-options=json.indent:2',
|
||||
'--unsorted',
|
||||
'--sorted',
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': True
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': True,
|
||||
'indent': 2,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
(
|
||||
[
|
||||
'--format-options=json.indent:2',
|
||||
'--sorted',
|
||||
'--no-sorted',
|
||||
'--no-unsorted',
|
||||
],
|
||||
{
|
||||
'headers': {
|
||||
'sort': True
|
||||
},
|
||||
'json': {
|
||||
'sort_keys': True,
|
||||
'indent': 2,
|
||||
'format': True
|
||||
},
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_format_options_accumulation(self, args, expected_format_options):
|
||||
parsed_args = parser.parse_args(
|
||||
args=[*args, 'example.org'],
|
||||
env=MockEnvironment(),
|
||||
)
|
||||
assert parsed_args.format_options == expected_format_options
|
||||
|
@ -1,14 +1,17 @@
|
||||
# coding=utf-8
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from tempfile import gettempdir
|
||||
|
||||
import pytest
|
||||
|
||||
from httpie.plugins.builtin import HTTPBasicAuth
|
||||
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK
|
||||
from fixtures import UNICODE
|
||||
from httpie.plugins.builtin import HTTPBasicAuth
|
||||
from httpie.sessions import Session
|
||||
from httpie.utils import get_expired_cookies
|
||||
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK
|
||||
|
||||
|
||||
class SessionTestBase:
|
||||
@ -186,3 +189,91 @@ class TestSession(SessionTestBase):
|
||||
httpbin.url + '/get', env=self.env())
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
class TestExpiredCookies:
|
||||
|
||||
def setup_method(self, method):
|
||||
self.config_dir = mk_config_dir()
|
||||
|
||||
def teardown_method(self, method):
|
||||
shutil.rmtree(self.config_dir)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['initial_cookie', 'expired_cookie'],
|
||||
argvalues=[
|
||||
({'id': {'value': 123}}, 'id'),
|
||||
({'id': {'value': 123}}, 'token')
|
||||
]
|
||||
)
|
||||
def test_removes_expired_cookies_from_session_obj(self, initial_cookie, expired_cookie, httpbin):
|
||||
session = Session(self.config_dir)
|
||||
session['cookies'] = initial_cookie
|
||||
session.remove_cookies([expired_cookie])
|
||||
assert expired_cookie not in session.cookies
|
||||
|
||||
def test_expired_cookies(self, httpbin):
|
||||
orig_session = {
|
||||
'cookies': {
|
||||
'to_expire': {
|
||||
'value': 'foo'
|
||||
},
|
||||
'to_stay': {
|
||||
'value': 'foo'
|
||||
},
|
||||
}
|
||||
}
|
||||
session_path = self.config_dir / 'test-session.json'
|
||||
session_path.write_text(json.dumps(orig_session))
|
||||
|
||||
r = http(
|
||||
'--session', str(session_path),
|
||||
'--print=H',
|
||||
httpbin.url + '/cookies/delete?to_expire',
|
||||
)
|
||||
assert 'Cookie: to_expire=foo; to_stay=foo' in r
|
||||
|
||||
updated_session = json.loads(session_path.read_text())
|
||||
assert 'to_stay' in updated_session['cookies']
|
||||
assert 'to_expire' not in updated_session['cookies']
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
argnames=['headers', 'now', 'expected_expired'],
|
||||
argvalues=[
|
||||
(
|
||||
[
|
||||
('Set-Cookie', 'hello=world; Path=/; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly'),
|
||||
('Connection', 'keep-alive')
|
||||
],
|
||||
None,
|
||||
[
|
||||
{
|
||||
'name': 'hello',
|
||||
'path': '/'
|
||||
}
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
('Set-Cookie', 'hello=world; Path=/; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly'),
|
||||
('Set-Cookie', 'pea=pod; Path=/ab; Expires=Thu, 01-Jan-1970 00:00:00 GMT; HttpOnly'),
|
||||
('Connection', 'keep-alive')
|
||||
],
|
||||
None,
|
||||
[
|
||||
{'name': 'hello', 'path': '/'},
|
||||
{'name': 'pea', 'path': '/ab'}
|
||||
]
|
||||
),
|
||||
(
|
||||
[
|
||||
('Set-Cookie', 'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly'),
|
||||
('Connection', 'keep-alive')
|
||||
],
|
||||
datetime(2020, 6, 11).timestamp(),
|
||||
[]
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired):
|
||||
assert get_expired_cookies(headers, now=now) == expected_expired
|
||||
|
@ -1,11 +1,9 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import pytest_httpbin.certs
|
||||
import requests.exceptions
|
||||
|
||||
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
|
||||
from utils import HTTP_OK, TESTS_ROOT, http
|
||||
|
||||
|
||||
@ -23,10 +21,12 @@ except ImportError:
|
||||
requests.exceptions.SSLError,
|
||||
)
|
||||
|
||||
CERTS_ROOT = TESTS_ROOT / 'client_certs'
|
||||
CLIENT_CERT = str(CERTS_ROOT / 'client.crt')
|
||||
CLIENT_KEY = str(CERTS_ROOT / 'client.key')
|
||||
CLIENT_PEM = str(CERTS_ROOT / 'client.pem')
|
||||
|
||||
|
||||
CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
|
||||
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
|
||||
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
|
||||
# FIXME:
|
||||
# We test against a local httpbin instance which uses a self-signed cert.
|
||||
# Requests without --verify=<CA_BUNDLE> will fail with a verification error.
|
||||
@ -34,7 +34,8 @@ CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
|
||||
CA_BUNDLE = pytest_httpbin.certs.where()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('ssl_version', SSL_VERSION_ARG_MAPPING.keys())
|
||||
@pytest.mark.parametrize('ssl_version',
|
||||
AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())
|
||||
def test_ssl_version(httpbin_secure, ssl_version):
|
||||
try:
|
||||
r = http(
|
||||
@ -113,3 +114,23 @@ class TestServerCert:
|
||||
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
|
||||
with pytest.raises(ssl_errors):
|
||||
http(httpbin_secure.url + '/get', '--verify', __file__)
|
||||
|
||||
|
||||
def test_ciphers(httpbin_secure):
|
||||
r = http(
|
||||
httpbin_secure.url + '/get',
|
||||
'--ciphers',
|
||||
DEFAULT_SSL_CIPHERS,
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
|
||||
|
||||
def test_ciphers_none_can_be_selected(httpbin_secure):
|
||||
r = http(
|
||||
httpbin_secure.url + '/get',
|
||||
'--ciphers',
|
||||
'__FOO__',
|
||||
tolerate_error_exit_status=True,
|
||||
)
|
||||
assert r.exit_status == ExitStatus.ERROR
|
||||
assert 'No cipher can be selected.' in r.stderr
|
||||
|
@ -17,27 +17,37 @@ class TestMultipartFormDataFileUpload:
|
||||
|
||||
def test_upload_ok(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
'test-file@%s' % FILE_PATH_ARG, 'foo=bar')
|
||||
f'test-file@{FILE_PATH_ARG}', 'foo=bar')
|
||||
assert HTTP_OK in r
|
||||
assert 'Content-Disposition: form-data; name="foo"' in r
|
||||
assert 'Content-Disposition: form-data; name="test-file";' \
|
||||
' filename="%s"' % os.path.basename(FILE_PATH) in r
|
||||
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
||||
assert FILE_CONTENT in r
|
||||
assert '"foo": "bar"' in r
|
||||
assert 'Content-Type: text/plain' in r
|
||||
|
||||
def test_upload_multiple_fields_with_the_same_name(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
'test-file@%s' % FILE_PATH_ARG,
|
||||
'test-file@%s' % FILE_PATH_ARG)
|
||||
f'test-file@{FILE_PATH_ARG}',
|
||||
f'test-file@{FILE_PATH_ARG}')
|
||||
assert HTTP_OK in r
|
||||
assert r.count('Content-Disposition: form-data; name="test-file";'
|
||||
' filename="%s"' % os.path.basename(FILE_PATH)) == 2
|
||||
f' filename="{os.path.basename(FILE_PATH)}"') == 2
|
||||
# Should be 4, but is 3 because httpbin
|
||||
# doesn't seem to support filed field lists
|
||||
assert r.count(FILE_CONTENT) in [3, 4]
|
||||
assert r.count('Content-Type: text/plain') == 2
|
||||
|
||||
def test_upload_custom_content_type(self, httpbin):
|
||||
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
|
||||
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon')
|
||||
assert HTTP_OK in r
|
||||
# Content type is stripped from the filename
|
||||
assert 'Content-Disposition: form-data; name="test-file";' \
|
||||
f' filename="{os.path.basename(FILE_PATH)}"' in r
|
||||
assert FILE_CONTENT in r
|
||||
assert 'Content-Type: image/vnd.microsoft.icon' in r
|
||||
|
||||
|
||||
class TestRequestBodyFromFilePath:
|
||||
"""
|
||||
|
@ -216,6 +216,7 @@ def http(
|
||||
add_to_args.append('--timeout=3')
|
||||
|
||||
complete_args = [program_name, *add_to_args, *args]
|
||||
# print(' '.join(complete_args))
|
||||
|
||||
def dump_stderr():
|
||||
stderr.seek(0)
|
||||
|
Reference in New Issue
Block a user