Compare commits

...

45 Commits
2.1.0 ... 2.2.0

Author SHA1 Message Date
5945845420 v2.2.0 2020-06-18 22:20:12 +02:00
3ee5b49256 Update README.rst 2020-06-18 10:58:13 +02:00
bb024757b6 Clarify config docs 2020-06-16 13:33:14 +02:00
d35864e79d Cleanup 2020-06-16 13:01:48 +02:00
8a106781be Add --sorted
Also add --no-(sorted|unsorted) to allow the documented resetting to default via --no-<option>.
2020-06-16 12:54:50 +02:00
23dd80563f Cleanup 2020-06-16 12:25:46 +02:00
2bab69d9fb Fix default value 2020-06-16 12:24:03 +02:00
826489950d Added --unsorted
It acts as a shortcut for --format-options=json.sort_keys:false,headers.sort:false

#128
2020-06-16 12:20:13 +02:00
b86598886e Added netrc support for auth plugins.
Enabled for --auth-type=basic and digest, 3rd parties may opt in.

This closes #718, closes #719, closes #852, and also closes #934
2020-06-16 11:05:00 +02:00
c240162cab Added a test that verifies .netrc is honored when only --auth-type is passed 2020-06-16 10:07:41 +02:00
26e29612f2 Update CHANGELOG.rst 2020-06-15 23:08:09 +02:00
37200eb055 Cleanup 2020-06-15 23:02:16 +02:00
9c68d7dd87 Remove expired cookies (#929)
* added a test for expiring cookies

* updated tests

* set up util for extracting expired cookies from response header

* Revert "updated tests"

This reverts commit a4eb5c4498.

* Revert "Revert "updated tests""

This reverts commit d242e21bce.

* added more functionality to get-expired-cookies

* add 'clear expired cookies' from session.json files

* refactored get_expired_cookies

* fixed formatting issues

* ensured key exists in cookie_header dict

* fixed linting errors

* removed unused import

* Added tests for get_expired_cookies util

* Added additional test for get_expired_cookies

* added remove_expired_cookies method directly to sessions class

* extracted logic to clear cookies to sessions.py

* refactored utils

* added tests to check expired cookies being removed from session obj

* added type annotations for methods

* Refactored test_sessions

* Seperated out expiry related tests into own class

* Refactored get_expired_cookies in utils

* Refactored remove cookie methods

* fixed linting errors

* fixed indentation and also pluralized test class name

* removed inheritance from SessionTestbase class

* Moved related test to TestExpiredCookies class

Co-authored-by: kbanc <katherine.bancoft@gmail.com>
2020-06-15 22:28:04 +02:00
7ee519ef46 Update CHANGELOG 2020-06-08 18:02:04 +02:00
c4627cc882 Custom file upload MIME type (#927)
* Support curl-like syntax for custom MIME type for files

In order to specify a custom MIME type for file uploads, a syntax
similar to that used by cURL is used so that

http -F test_file@/path/to/file.bin;type=application/zip https://...

forwards the user-provided file type if provided, otherwise falling
back to the usual guesswork out of the file extension.
2020-06-08 17:59:41 +02:00
492687b0da Add stable docs link icon 2020-05-28 14:30:56 +02:00
caeef2fb7c Use : instead of = in `--format-options 2020-05-28 14:24:15 +02:00
aae596d472 Improve --format-options error messages 2020-05-27 16:19:32 +02:00
cb51faec51 pep8 2020-05-27 16:12:31 +02:00
c2a0cef76e Add --format-options to allow disabling sorting, etc.
#128
2020-05-27 16:01:17 +02:00
493e98c833 Update CHANGELOG 2020-05-26 10:15:33 +02:00
ca02e51420 Improve plugin API docs 2020-05-26 10:07:53 +02:00
cd085cbc0d Refactor built-in plugin registry to avoid circular imports
Fix #925
2020-05-26 10:07:34 +02:00
27d57ce773 Cleanup 2020-05-23 20:30:25 +02:00
4c4efff56a Pass cert_reqs to context 2020-05-23 20:19:16 +02:00
a53505f26e Fix SSL context 2020-05-23 15:01:33 +02:00
165dc36f8d Add examples 2020-05-23 13:38:28 +02:00
5df3a91619 Add examples 2020-05-23 13:37:47 +02:00
7dbceafc01 Add docs for the https command alias 2020-05-23 13:34:59 +02:00
d62d6a77d1 Add support for --ciphers (#870) 2020-05-23 13:26:06 +02:00
0a81facccf Str env vars 2020-05-23 12:14:09 +02:00
3e20ade645 Cleanup & refactor XDG_CONFIG_HOME support 2020-05-23 12:12:35 +02:00
0c47094109 Update CHANGELOG.rst 2020-05-22 12:38:42 +02:00
defe4bc76d Fix issue links 2020-05-21 16:03:40 +02:00
afee6a7970 Added changelog entry for $XDG_CONFIG_HOME support 2020-05-21 15:59:03 +02:00
7b676dd583 Update ~/.httpie references to ~/.config/httpie 2020-05-21 15:56:53 +02:00
5af0874ed3 Support (part of) the XDG Base Directory Specification (#920)
On Unix-like systems, the configuration file now lives in
$XDG_CONFIG_HOME/httpie/ by default, not ~/.httpie/ (the behaviour on
Windows is unchanged). The previous location is still checked, in order
to support existing installations.

Searching $XDG_CONFIG_DIRS is still not supported.

Fixes #145; supersedes #436.
2020-05-21 15:50:00 +02:00
e11a2d1346 Update FUNDING.yml 2020-05-13 21:55:24 +02:00
b2044fc18d Update README.rst 2020-04-24 12:15:19 +02:00
d9a2d665ad Fix typo (#898) 2020-04-20 17:46:43 +02:00
e83e275dff Fix spelling of “GitHub” (#899) 2020-04-20 17:45:51 +02:00
4a99495466 Update CHANGELOG.rst 2020-04-18 20:44:40 +02:00
495f67229a Fix brew formula 2020-04-18 13:39:17 +02:00
45b9bae3dc Update brew formula 2020-04-18 13:24:25 +02:00
774ff148cd 2.2.0-dev 2020-04-18 12:57:56 +02:00
35 changed files with 1144 additions and 267 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,6 +1,6 @@
# These are supported funding model platforms # 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 patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

2
.gitignore vendored
View File

@ -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> # <https://github.com/github/gitignore/blob/master/Python.gitignore>
############################################################################## ##############################################################################

View File

@ -6,16 +6,30 @@ This document records all notable changes to `HTTPie <https://httpie.org>`_.
This project adheres to `Semantic Versioning <https://semver.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) `2.1.0`_ (2020-04-18)
--------------------- ---------------------
* Added ``--path-as-is`` to bypass dot segment (``/../`` or ``/./``) * Added ``--path-as-is`` to bypass dot segment (``/../`` or ``/./``)
URL squashing (#895). URL squashing (`#895`_).
* Changed the default value ``Accept`` header value for JSON requests from * Changed the default ``Accept`` header value for JSON requests from
``application/json, */*`` to ``application/json, */*;q=0.5`` ``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 * Fixed ``--form`` file upload mixed with redirected ``stdin`` error handling
(#840). (`#840`_).
`2.0.0`_ (2020-01-12) `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``. ``$ alias https='http --default-scheme=https``.
* Added ``-I`` as a shortcut for ``--ignore-stdin``. * Added ``-I`` as a shortcut for ``--ignore-stdin``.
* Added fish shell completion (located in ``extras/httpie-completion.fish`` * 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 * Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
``pip install requests[socks]``. ``pip install requests[socks]``.
* Changed the default JSON ``Accept`` header from ``application/json`` * 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 .. _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.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.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 .. _#128: https://github.com/jakubroztocil/httpie/issues/128
.. _#840:https://github.com/jakubroztocil/httpie/issues/840 .. _#488: https://github.com/jakubroztocil/httpie/issues/488
.. _#895:https://github.com/jakubroztocil/httpie/issues/895 .. _#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

View File

@ -115,7 +115,7 @@ Testing & CI
Please add tests for any new features and bug fixes. Please add tests for any new features and bug fixes.
When you open a pull request, 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 HTTPies `test suite`_ against your code will automatically run HTTPies `test suite`_ against your code
so please make sure all checks pass. so please make sure all checks pass.

View File

@ -11,7 +11,7 @@ generally interacting with HTTP servers.
.. class:: no-web no-pdf .. class:: no-web no-pdf
|pypi| |build| |coverage| |downloads| |gitter| |docs| |pypi| |build| |coverage| |downloads| |gitter|
.. class:: no-web no-pdf .. 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 $ 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>`_ `issue <https://github.com/jakubroztocil/httpie/issues/83>`_
with `authentication`_: with `authentication`_:
@ -305,8 +305,23 @@ Request URL
=========== ===========
The only information HTTPie needs to perform a request is a 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 Querystring parameters
@ -668,6 +683,13 @@ submitted:
Note that ``@`` is used to simulate a file upload form field, whereas Note that ``@`` is used to simulate a file upload form field, whereas
``=@`` just embeds the file content as a regular text field value. ``=@`` 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 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 SSL version
----------- -----------
Use the ``--ssl=<PROTOCOL>`` to specify the desired protocol version to use. Use the ``--ssl=<PROTOCOL>`` option to specify the desired protocol version to
This will default to SSL v2.3 which will negotiate the highest protocol that both use. This will default to SSL v2.3 which will negotiate the highest protocol
the server and your installation of OpenSSL support. The available protocols that both the server and your installation of OpenSSL support. The available
are ``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ``tls1.2``, ``tls1.3``. (The actually protocols are
available set of protocols may vary depending on your OpenSSL installation.) ``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 .. 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 $ 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 Output options
============== ==============
@ -1358,6 +1402,26 @@ One of these options can be used to control output processing:
Default for redirected output. 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 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 $ http --session=user2 -a user2:password httpbin.org/get X-Bar:Foo
Named sessionss data is stored in JSON files in the the ``sessions`` Named sessionss data is stored in JSON files inside the ``sessions``
subdirectory of the `config`_ directory: subdirectory of the `config`_ directory, typically:
``~/.httpie/sessions/<host>/<name>.json`` ``~/.config/httpie/sessions/<host>/<name>.json``
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows). (``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
If you have executed the above commands on a unix machine, 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 .. code-block:: bash
$ ls -l ~/.httpie/sessions/httpbin.org $ ls -l ~/.config/httpie/sessions/httpbin.org
Anonymous sessions Anonymous sessions
@ -1655,7 +1719,7 @@ allows for sessions to be re-used across multiple hosts:
.. code-block:: bash .. code-block:: bash
# You can also refer to a previously created named session: # 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 When creating anonymous sessions, please remember to always include at least
@ -1691,8 +1755,17 @@ but you can create it manually.
Config file directory Config file directory
--------------------- ---------------------
The default location of the configuration file is ``~/.httpie/config.json`` To see the exact location for your installation, run ``http --debug`` and
(or ``%APPDATA%\httpie\config.json`` on Windows). 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`` The config directory can be changed by setting the ``$HTTPIE_CONFIG_DIR``
environment variable: environment variable:
@ -1702,7 +1775,6 @@ environment variable:
$ export HTTPIE_CONFIG_DIR=/tmp/httpie $ export HTTPIE_CONFIG_DIR=/tmp/httpie
$ http httpbin.org/get $ http httpbin.org/get
To view the exact location run ``http --debug``.
Configurable options Configurable options
@ -1722,7 +1794,7 @@ For instance, you can use this config option to change your default color theme:
.. code-block:: bash .. code-block:: bash
$ cat ~/.httpie/config.json $ cat ~/.config/httpie/config.json
.. code-block:: json .. code-block:: json
@ -1933,12 +2005,16 @@ have contributed.
.. _pip: https://pip.pypa.io/en/stable/installing/ .. _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 .. _these fine people: https://github.com/jakubroztocil/httpie/contributors
.. _Jakub Roztocil: https://roztocil.co .. _Jakub Roztocil: https://roztocil.co
.. _@jakubroztocil: https://twitter.com/jakubroztocil .. _@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 .. |pypi| image:: https://img.shields.io/pypi/v/httpie.svg?style=flat-square&label=latest%20stable%20version
:target: https://pypi.python.org/pypi/httpie :target: https://pypi.python.org/pypi/httpie
:alt: Latest version released on PyPi :alt: Latest version released on PyPi

View File

@ -9,43 +9,42 @@ class Httpie < Formula
desc "User-friendly cURL replacement (command-line HTTP client)" desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.org/" homepage "https://httpie.org/"
url "https://files.pythonhosted.org/packages/35/6c/93da2ebd4eb768c3733437ce01b5fae297522434fdeabeeabdc4f42aabd3/httpie-2.0.0.tar.gz" url "https://files.pythonhosted.org/packages/e2/79/da6aec7b4356e8b325561b987c940e5b1e4de1200a5c3db7c57a97d61ca1/httpie-2.1.0.tar.gz"
sha256 "8c04f9756f1a7eac71a6dfa0834d0f6813dc8a982d8564f3a7418dcd19107c09" sha256 "a76f1c72e83bd03cde3478c5f345d5570fdb2967ed19d68d09518088640b9e8e"
revision 1
head "https://github.com/jakubroztocil/httpie.git" head "https://github.com/jakubroztocil/httpie.git"
bottle do bottle do
cellar :any_skip_relocation cellar :any_skip_relocation
sha256 "19694b5ec311939a8b73cc329ca49386155ed3a17e4eca691779c725d36286b5" => :catalina sha256 "1fb33d9c85dc462c2549a03cf08670edad8014a5fdf0a7cb26493c64af40283d" => :catalina
sha256 "8c7d93d55ea3351e25fadfdd3748ca0a3ff7dd62ab9dbf31b7243fba76890c4d" => :mojave sha256 "a22030f0b96c698c90265286ee80ffbb03079d1d008a80c0bdb3ea15a17d3fbb" => :mojave
sha256 "e0d5269bb5d03a1797c8612005fa46d6a35f2b84eb76e9607ef1169464b566ea" => :high_sierra sha256 "9f994ecf826efe53a3a49d1c3193e271629068d11306df55adeea2842a8afb8c" => :high_sierra
end end
depends_on "python@3.8" depends_on "python@3.8"
resource "Pygments" do resource "Pygments" do
url "https://files.pythonhosted.org/packages/cb/9f/27d4844ac5bf158a33900dbad7985951e2910397998e85712da03ce125f0/Pygments-2.5.2.tar.gz" url "https://files.pythonhosted.org/packages/6e/4d/4d2fe93a35dfba417311a4ff627489a947b01dc0cc377a3673c00cf7e4b2/Pygments-2.6.1.tar.gz"
sha256 "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" sha256 "647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"
end end
resource "requests" do resource "requests" do
url "https://files.pythonhosted.org/packages/01/62/ddcf76d1d19885e8579acb1b1df26a852b03472c0e46d2b959a714c90608/requests-2.22.0.tar.gz" url "https://files.pythonhosted.org/packages/f5/4f/280162d4bd4d8aad241a21aecff7a6e46891b905a4341e7ab549ebaf7915/requests-2.23.0.tar.gz"
sha256 "11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4" sha256 "b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
end end
resource "certifi" do resource "certifi" do
url "https://files.pythonhosted.org/packages/41/bf/9d214a5af07debc6acf7f3f257265618f1db242a3f8e49a9b516f24523a6/certifi-2019.11.28.tar.gz" url "https://files.pythonhosted.org/packages/b8/e2/a3a86a67c3fc8249ed305fc7b7d290ebe5e4d46ad45573884761ef4dea7b/certifi-2020.4.5.1.tar.gz"
sha256 "25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" sha256 "51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
end end
resource "urllib3" do resource "urllib3" do
url "https://files.pythonhosted.org/packages/ad/fc/54d62fa4fc6e675678f9519e677dfc29b8964278d75333cf142892caf015/urllib3-1.25.7.tar.gz" url "https://files.pythonhosted.org/packages/05/8c/40cd6949373e23081b3ea20d5594ae523e681b6f472e600fbc95ed046a36/urllib3-1.25.9.tar.gz"
sha256 "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" sha256 "3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"
end end
resource "idna" do resource "idna" do
url "https://files.pythonhosted.org/packages/ad/13/eb56951b6f7950cadb579ca166e448ba77f9d24efc03edd7e55fa57d04b7/idna-2.8.tar.gz" url "https://files.pythonhosted.org/packages/cb/19/57503b5de719ee45e83472f339f617b0c01ad75cba44aba1e4c97c2b0abd/idna-2.9.tar.gz"
sha256 "c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407" sha256 "7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"
end end
resource "chardet" do resource "chardet" do

View File

@ -3,6 +3,6 @@ HTTPie - a CLI, cURL-like tool for humans.
""" """
__version__ = '2.1.0' __version__ = '2.2.0'
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -7,9 +7,16 @@ from argparse import RawDescriptionHelpFormatter
from textwrap import dedent from textwrap import dedent
from urllib.parse import urlsplit 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 ( 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, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED, OUT_RESP_BODY, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS, PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS,
SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE, 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.exceptions import ParseError
from httpie.cli.requestitems import RequestItems from httpie.cli.requestitems import RequestItems
from httpie.context import Environment 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 from httpie.utils import ExplicitNullAuth, get_content_type
@ -42,6 +49,8 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
return text.splitlines() return text.splitlines()
# TODO: refactor and design type-annotated data structures
# for raw args + parsed args and keep things immutable.
class HTTPieArgumentParser(argparse.ArgumentParser): class HTTPieArgumentParser(argparse.ArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`. """Adds additional logic to `argparse.ArgumentParser`.
@ -82,6 +91,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self._setup_standard_streams() self._setup_standard_streams()
self._process_output_options() self._process_output_options()
self._process_pretty_options() self._process_pretty_options()
self._process_format_options()
self._guess_method() self._guess_method()
self._parse_items() self._parse_items()
@ -154,7 +164,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.env.stdout_isatty = False self.env.stdout_isatty = False
def _process_auth(self): def _process_auth(self):
# TODO: refactor # TODO: refactor & simplify this method.
self.args.auth_plugin = None self.args.auth_plugin = None
default_auth_plugin = plugin_manager.get_auth_plugins()[0] default_auth_plugin = plugin_manager.get_auth_plugins()[0]
auth_type_set = self.args.auth_type is not None 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 self.args.auth_type = default_auth_plugin.auth_type
plugin = plugin_manager.get_auth_plugin(self.args.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 its 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: if plugin.auth_require and self.args.auth is None:
self.error('--auth required') self.error('--auth required')
@ -390,3 +413,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
if self.args.download_resume and not ( if self.args.download_resume and not (
self.args.download and self.args.output_file): self.args.download and self.args.output_file):
self.error('--continue requires --output to be specified') 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

View File

@ -2,9 +2,10 @@ import argparse
import getpass import getpass
import os import os
import sys 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 from httpie.sessions import VALID_SESSION_NAME_PATTERN
@ -181,3 +182,69 @@ def readable_file_arg(filename):
return filename return filename
except IOError as ex: except IOError as ex:
raise argparse.ArgumentTypeError(f'{filename}: {ex.args[1]}') 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

View File

@ -24,6 +24,7 @@ SEPARATOR_PROXY = ':'
SEPARATOR_DATA_STRING = '=' SEPARATOR_DATA_STRING = '='
SEPARATOR_DATA_RAW_JSON = ':=' SEPARATOR_DATA_RAW_JSON = ':='
SEPARATOR_FILE_UPLOAD = '@' SEPARATOR_FILE_UPLOAD = '@'
SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@' SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@' SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
SEPARATOR_QUERY_PARAM = '==' SEPARATOR_QUERY_PARAM = '=='
@ -83,21 +84,22 @@ PRETTY_MAP = {
} }
PRETTY_STDOUT_TTY_ONLY = object() 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 # 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
OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_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)
}

View File

@ -8,25 +8,29 @@ from textwrap import dedent, wrap
from httpie import __doc__, __version__ from httpie import __doc__, __version__
from httpie.cli.argparser import HTTPieArgumentParser from httpie.cli.argparser import HTTPieArgumentParser
from httpie.cli.argtypes import ( from httpie.cli.argtypes import (
KeyValueArgType, SessionNameValidator, readable_file_arg, KeyValueArgType, SessionNameValidator,
readable_file_arg,
) )
from httpie.cli.constants import ( 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, 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 ( from httpie.output.formatters.colors import (
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE, AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
) )
from httpie.plugins import plugin_manager
from httpie.plugins.builtin import BuiltinAuthPlugin from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.plugins.registry import plugin_manager
from httpie.sessions import DEFAULT_SESSIONS_DIR from httpie.sessions import DEFAULT_SESSIONS_DIR
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
parser = HTTPieArgumentParser( parser = HTTPieArgumentParser(
prog='http', prog='http',
description='%s <https://httpie.org>' % __doc__.strip(), description='%s <https://httpie.org>' % __doc__.strip(),
epilog=dedent(""" epilog=dedent('''
For every --OPTION there is also a --no-OPTION that reverts OPTION For every --OPTION there is also a --no-OPTION that reverts OPTION
to its default value. to its default value.
@ -34,7 +38,7 @@ parser = HTTPieArgumentParser(
https://github.com/jakubroztocil/httpie/issues https://github.com/jakubroztocil/httpie/issues
"""), '''),
) )
####################################################################### #######################################################################
@ -43,18 +47,18 @@ parser = HTTPieArgumentParser(
positional = parser.add_argument_group( positional = parser.add_argument_group(
title='Positional Arguments', title='Positional Arguments',
description=dedent(""" description=dedent('''
These arguments come after any flags and in the order they are listed here. These arguments come after any flags and in the order they are listed here.
Only URL is required. Only URL is required.
""") ''')
) )
positional.add_argument( positional.add_argument(
dest='method', dest='method',
metavar='METHOD', metavar='METHOD',
nargs=OPTIONAL, nargs=OPTIONAL,
default=None, default=None,
help=""" help='''
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...). 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 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 # => GET
$ http example.org hello=world # => POST $ http example.org hello=world # => POST
""" '''
) )
positional.add_argument( positional.add_argument(
dest='url', dest='url',
metavar='URL', metavar='URL',
help=""" help='''
The scheme defaults to 'http://' if the URL does not include one. The scheme defaults to 'http://' if the URL does not include one.
(You can override this with: --default-scheme=https) (You can override this with: --default-scheme=https)
@ -77,7 +81,7 @@ positional.add_argument(
$ http :3000 # => http://localhost:3000 $ http :3000 # => http://localhost:3000
$ http :/foo # => http://localhost/foo $ http :/foo # => http://localhost/foo
""" '''
) )
positional.add_argument( positional.add_argument(
dest='request_items', dest='request_items',
@ -85,7 +89,7 @@ positional.add_argument(
nargs=ZERO_OR_MORE, nargs=ZERO_OR_MORE,
default=None, default=None,
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS), type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
help=r""" help=r'''
Optional key-value pairs to be included in the request. The separator used Optional key-value pairs to be included in the request. The separator used
determines the type: determines the type:
@ -108,7 +112,8 @@ positional.add_argument(
'@' Form file fields (only with --form, -f): '@' 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: '=@' 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 field-name-with\:colon=value
""" '''
) )
####################################################################### #######################################################################
@ -137,24 +142,24 @@ content_type = parser.add_argument_group(
content_type.add_argument( content_type.add_argument(
'--json', '-j', '--json', '-j',
action='store_true', action='store_true',
help=""" help='''
(default) Data items from the command line are serialized as a JSON object. (default) Data items from the command line are serialized as a JSON object.
The Content-Type and Accept headers are set to application/json The Content-Type and Accept headers are set to application/json
(if not specified). (if not specified).
""" '''
) )
content_type.add_argument( content_type.add_argument(
'--form', '-f', '--form', '-f',
action='store_true', 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 (if not The Content-Type is set to application/x-www-form-urlencoded (if not
specified). The presence of any file fields results in a specified). The presence of any file fields results in a
multipart/form-data request. multipart/form-data request.
""" '''
) )
####################################################################### #######################################################################
@ -170,14 +175,14 @@ content_processing.add_argument(
'--compress', '-x', '--compress', '-x',
action='count', action='count',
default=0, default=0,
help=""" help='''
Content compressed (encoded) with Deflate algorithm. Content compressed (encoded) with Deflate algorithm.
The Content-Encoding header is set to deflate. The Content-Encoding header is set to deflate.
Compression is skipped if it appears that compression ratio is Compression is skipped if it appears that compression ratio is
negative. Compression can be forced by repeating the argument. negative. Compression can be forced by repeating the argument.
""" '''
) )
####################################################################### #######################################################################
@ -191,12 +196,12 @@ output_processing.add_argument(
dest='prettify', dest='prettify',
default=PRETTY_STDOUT_TTY_ONLY, default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()), choices=sorted(PRETTY_MAP.keys()),
help=""" help='''
Controls output processing. The value can be "none" to not prettify Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors the output (default for redirected output), "all" to apply both colors
and formatting (default for terminal output), "colors", or "format". and formatting (default for terminal output), "colors", or "format".
""" '''
) )
output_processing.add_argument( output_processing.add_argument(
'--style', '-s', '--style', '-s',
@ -204,10 +209,10 @@ output_processing.add_argument(
metavar='STYLE', metavar='STYLE',
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
choices=AVAILABLE_STYLES, choices=AVAILABLE_STYLES,
help=""" help='''
Output coloring style (default is "{default}"). One of: 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. 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 $TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc). (e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
""".format( '''.format(
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
available_styles='\n'.join( available_styles='\n'.join(
'{0}{1}'.format(8 * ' ', line.strip()) '{0}{1}'.format(8 * ' ', line.strip())
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60) for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
).rstrip(), ).strip(),
auto_style=AUTO_STYLE, 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 # Output options
@ -234,7 +299,7 @@ output_options.add_argument(
'--print', '-p', '--print', '-p',
dest='output_options', dest='output_options',
metavar='WHAT', metavar='WHAT',
help=f""" help=f'''
String specifying what the output should contain: String specifying what the output should contain:
'{OUT_REQ_HEAD}' request headers '{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 If the output is piped to another program or to a file, then only the
response body is printed by default. response body is printed by default.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--headers', '-h', '--headers', '-h',
dest='output_options', dest='output_options',
action='store_const', action='store_const',
const=OUT_RESP_HEAD, const=OUT_RESP_HEAD,
help=f""" help=f'''
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}. Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--body', '-b', '--body', '-b',
dest='output_options', dest='output_options',
action='store_const', action='store_const',
const=OUT_RESP_BODY, const=OUT_RESP_BODY,
help=f""" help=f'''
Print only the response body. Shortcut for --print={OUT_RESP_BODY}. Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--verbose', '-v', '--verbose', '-v',
dest='verbose', dest='verbose',
action='store_true', action='store_true',
help=""" help='''
Verbose output. Print the whole request as well as the response. Also print Verbose output. Print the whole request as well as the response. Also print
any intermediary requests/responses (such as redirects). any intermediary requests/responses (such as redirects).
It's a shortcut for: --all --print={0} It's a shortcut for: --all --print={0}
""".format(''.join(OUTPUT_OPTIONS)) '''.format(''.join(OUTPUT_OPTIONS))
) )
output_options.add_argument( output_options.add_argument(
'--all', '--all',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
By default, only the final request/response is shown. Use this flag to show By default, only the final request/response is shown. Use this flag to show
any intermediary requests/responses as well. Intermediary requests include any intermediary requests/responses as well. Intermediary requests include
followed redirects (with --follow), the first unauthorized request when followed redirects (with --follow), the first unauthorized request when
Digest auth is used (--auth=digest), etc. Digest auth is used (--auth=digest), etc.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--history-print', '-P', '--history-print', '-P',
dest='output_options_history', dest='output_options_history',
metavar='WHAT', metavar='WHAT',
help=""" help='''
The same as --print, -p but applies only to intermediary requests/responses The same as --print, -p but applies only to intermediary requests/responses
(such as redirects) when their inclusion is enabled with --all. If this (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 options is not specified, then they are formatted the same way as the final
response. response.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--stream', '-S', '--stream', '-S',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Always stream the output by line, i.e., behave like `tail -f'. Always stream the output by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied), 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 It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks. more often and in smaller chunks.
""" '''
) )
output_options.add_argument( output_options.add_argument(
'--output', '-o', '--output', '-o',
type=FileType('a+b'), type=FileType('a+b'),
dest='output_file', dest='output_file',
metavar='FILE', metavar='FILE',
help=""" help='''
Save output to FILE instead of stdout. If --download is also set, then only 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 the response body is saved to FILE. Other parts of the HTTP exchange are
printed to stderr. printed to stderr.
""" '''
) )
@ -341,12 +406,12 @@ output_options.add_argument(
'--download', '-d', '--download', '-d',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Do not print the response body to stdout. Rather, download it and store it 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 in a file. The filename is guessed unless specified with --output
[filename]. This action is similar to the default behaviour of wget. [filename]. This action is similar to the default behaviour of wget.
""" '''
) )
output_options.add_argument( output_options.add_argument(
@ -354,11 +419,11 @@ output_options.add_argument(
dest='download_resume', dest='download_resume',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Resume an interrupted download. Note that the --output option needs to be Resume an interrupted download. Note that the --output option needs to be
specified as well. specified as well.
""" '''
) )
####################################################################### #######################################################################
@ -376,7 +441,7 @@ sessions.add_argument(
'--session', '--session',
metavar='SESSION_NAME_OR_PATH', metavar='SESSION_NAME_OR_PATH',
type=session_name_validator, type=session_name_validator,
help=f""" help=f'''
Create, or reuse and update a session. Within a session, custom headers, 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 auth credential, as well as any cookies sent by the server persist between
requests. requests.
@ -385,17 +450,17 @@ sessions.add_argument(
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json. {DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
""" '''
) )
sessions.add_argument( sessions.add_argument(
'--session-read-only', '--session-read-only',
metavar='SESSION_NAME_OR_PATH', metavar='SESSION_NAME_OR_PATH',
type=session_name_validator, type=session_name_validator,
help=""" help='''
Create or read a session without updating it form the request/response Create or read a session without updating it form the request/response
exchange. exchange.
""" '''
) )
####################################################################### #######################################################################
@ -408,11 +473,11 @@ auth.add_argument(
'--auth', '-a', '--auth', '-a',
default=None, default=None,
metavar='USER[:PASS]', metavar='USER[:PASS]',
help=""" help='''
If only the username is provided (-a username), HTTPie will prompt If only the username is provided (-a username), HTTPie will prompt
for the password. for the password.
""", ''',
) )
@ -431,12 +496,12 @@ auth.add_argument(
'--auth-type', '-A', '--auth-type', '-A',
choices=_AuthTypeLazyChoices(), choices=_AuthTypeLazyChoices(),
default=None, default=None,
help=""" help='''
The authentication mechanism to be used. Defaults to "{default}". The authentication mechanism to be used. Defaults to "{default}".
{types} {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}": {name}{package}{description}'.format(
type=plugin.auth_type, type=plugin.auth_type,
name=plugin.name, name=plugin.name,
@ -456,10 +521,10 @@ auth.add_argument(
'--ignore-netrc', '--ignore-netrc',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
Ignore credentials from .netrc. Ignore credentials from .netrc.
""", ''',
) )
####################################################################### #######################################################################
@ -472,9 +537,9 @@ network.add_argument(
'--offline', '--offline',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
Build the request and print it but dont actually send it. Build the request and print it but dont actually send it.
""" '''
) )
network.add_argument( network.add_argument(
'--proxy', '--proxy',
@ -482,43 +547,43 @@ network.add_argument(
action='append', action='append',
metavar='PROTOCOL:PROXY_URL', metavar='PROTOCOL:PROXY_URL',
type=KeyValueArgType(SEPARATOR_PROXY), type=KeyValueArgType(SEPARATOR_PROXY),
help=""" help='''
String mapping protocol to the URL of the proxy String mapping protocol to the URL of the proxy
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with (e.g. http:http://foo.bar:3128). You can specify multiple proxies with
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY, different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
and $HTTPS_proxy are supported as well. and $HTTPS_proxy are supported as well.
""" '''
) )
network.add_argument( network.add_argument(
'--follow', '-F', '--follow', '-F',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
Follow 30x Location redirects. Follow 30x Location redirects.
""" '''
) )
network.add_argument( network.add_argument(
'--max-redirects', '--max-redirects',
type=int, type=int,
default=30, default=30,
help=""" help='''
By default, requests have a limit of 30 redirects (works with --follow). By default, requests have a limit of 30 redirects (works with --follow).
""" '''
) )
network.add_argument( network.add_argument(
'--max-headers', '--max-headers',
type=int, type=int,
default=0, default=0,
help=""" help='''
The maximum number of response headers to be read before giving up The maximum number of response headers to be read before giving up
(default 0, i.e., no limit). (default 0, i.e., no limit).
""" '''
) )
network.add_argument( network.add_argument(
@ -526,7 +591,7 @@ network.add_argument(
type=float, type=float,
default=0, default=0,
metavar='SECONDS', metavar='SECONDS',
help=""" help='''
The connection timeout of the request in seconds. The connection timeout of the request in seconds.
The default value is 0, i.e., there is no timeout limit. The default value is 0, i.e., there is no timeout limit.
This is not a time limit on the entire response download; 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 timeout seconds (more precisely, if no bytes have been received on
the underlying socket for timeout seconds). the underlying socket for timeout seconds).
""" '''
) )
network.add_argument( network.add_argument(
'--check-status', '--check-status',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
By default, HTTPie exits with 0 when no network or other fatal errors 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 occur. This flag instructs HTTPie to also check the HTTP status code and
exit with an error if the status indicates one. 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. 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. Also an error message is written to stderr if stdout is redirected.
""" '''
) )
network.add_argument( network.add_argument(
'--path-as-is', '--path-as-is',
default=False, default=False,
action='store_true', action='store_true',
help=""" help='''
Bypass dot segment (/../ or /./) URL squashing. Bypass dot segment (/../ or /./) URL squashing.
""" '''
) )
####################################################################### #######################################################################
@ -570,47 +635,58 @@ ssl = parser.add_argument_group(title='SSL')
ssl.add_argument( ssl.add_argument(
'--verify', '--verify',
default='yes', default='yes',
help=""" help='''
Set to "no" (or "false") to skip checking the host's SSL certificate. 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 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 for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
variable instead.) variable instead.)
""" '''
) )
ssl.add_argument( ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol? '--ssl', # TODO: Maybe something more general, such as --secure-protocol?
dest='ssl_version', dest='ssl_version',
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())), choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())),
help=""" help='''
The desired protocol version to use. This will default to The desired protocol version to use. This will default to
SSL v2.3 which will negotiate the highest protocol that both SSL v2.3 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support. Available protocols the server and your installation of OpenSSL support. Available protocols
may vary depending on OpenSSL installation (only the supported ones may vary depending on OpenSSL installation (only the supported ones
are shown here). 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( ssl.add_argument(
'--cert', '--cert',
default=None, default=None,
type=readable_file_arg, type=readable_file_arg,
help=""" help='''
You can specify a local cert to use as client side SSL certificate. 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 This file may either contain both private key and certificate or you may
specify --cert-key separately. specify --cert-key separately.
""" '''
) )
ssl.add_argument( ssl.add_argument(
'--cert-key', '--cert-key',
default=None, default=None,
type=readable_file_arg, type=readable_file_arg,
help=""" help='''
The private key to use with SSL. Only needed if --cert is given and the The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key. certificate file does not contain the private key.
""" '''
) )
####################################################################### #######################################################################
@ -623,53 +699,53 @@ troubleshooting.add_argument(
'--ignore-stdin', '-I', '--ignore-stdin', '-I',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Do not attempt to read stdin. Do not attempt to read stdin.
""" '''
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--help', '--help',
action='help', action='help',
default=SUPPRESS, default=SUPPRESS,
help=""" help='''
Show this help message and exit. Show this help message and exit.
""" '''
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--version', '--version',
action='version', action='version',
version=__version__, version=__version__,
help=""" help='''
Show version and exit. Show version and exit.
""" '''
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--traceback', '--traceback',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Prints the exception traceback should one occur. Prints the exception traceback should one occur.
""" '''
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--default-scheme', '--default-scheme',
default="http", default="http",
help=""" help='''
The default scheme to use if not specified in the URL. The default scheme to use if not specified in the URL.
""" '''
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--debug', '--debug',
action='store_true', action='store_true',
default=False, default=False,
help=""" help='''
Prints the exception traceback should one occur, as well as other Prints the exception traceback should one occur, as well as other
information useful for debugging HTTPie itself and for reporting bugs. information useful for debugging HTTPie itself and for reporting bugs.
""" '''
) )

View File

@ -6,7 +6,8 @@ from httpie.cli.argtypes import KeyValueArg
from httpie.cli.constants import ( from httpie.cli.constants import (
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE, SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD, 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 ( from httpie.cli.dicts import (
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict, 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]: 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: try:
with open(os.path.expanduser(filename), 'rb') as f: with open(os.path.expanduser(filename), 'rb') as f:
contents = f.read() contents = f.read()
@ -104,7 +108,7 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
return ( return (
os.path.basename(filename), os.path.basename(filename),
BytesIO(contents), BytesIO(contents),
get_content_type(filename), mime_type or get_content_type(filename),
) )

View File

@ -9,24 +9,17 @@ from typing import Iterable, Union
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
from requests.adapters import HTTPAdapter # noinspection PyPackageRequirements
import urllib3
from httpie import __version__ from httpie import __version__
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
from httpie.cli.dicts import RequestHeadersDict 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.sessions import get_httpie_session
from httpie.utils import repr_dict from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
from httpie.utils import get_expired_cookies, repr_dict
try:
# noinspection PyPackageRequirements
import urllib3
# <https://urllib3.readthedocs.io/en/latest/security.html>
urllib3.disable_warnings()
except (ImportError, AttributeError):
pass
urllib3.disable_warnings()
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8' FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
JSON_CONTENT_TYPE = 'application/json' JSON_CONTENT_TYPE = 'application/json'
@ -57,6 +50,8 @@ def collect_messages(
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args) send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
requests_session = build_requests_session( requests_session = build_requests_session(
ssl_version=args.ssl_version, ssl_version=args.ssl_version,
ciphers=args.ciphers,
verify=bool(send_kwargs_mergeable_from_env['verify'])
) )
if httpie_session: if httpie_session:
@ -86,6 +81,7 @@ def collect_messages(
if args.compress and prepared_request.body: if args.compress and prepared_request.body:
compress_body(prepared_request, always=args.compress > 1) compress_body(prepared_request, always=args.compress > 1)
response_count = 0 response_count = 0
expired_cookies = []
while prepared_request: while prepared_request:
yield prepared_request yield prepared_request
if not args.offline: if not args.offline:
@ -99,6 +95,12 @@ def collect_messages(
**send_kwargs_merged, **send_kwargs_merged,
**send_kwargs, **send_kwargs,
) )
# noinspection PyProtectedMember
expired_cookies += get_expired_cookies(
headers=response.raw._original_response.msg._headers
)
response_count += 1 response_count += 1
if response.next: if response.next:
if args.max_redirects and response_count == args.max_redirects: if args.max_redirects and response_count == args.max_redirects:
@ -114,6 +116,10 @@ def collect_messages(
if httpie_session: if httpie_session:
if httpie_session.is_new() or not args.session_read_only: if httpie_session.is_new() or not args.session_read_only:
httpie_session.cookies = requests_session.cookies 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() httpie_session.save()
@ -121,6 +127,7 @@ def collect_messages(
@contextmanager @contextmanager
def max_headers(limit): def max_headers(limit):
# <https://github.com/jakubroztocil/httpie/issues/802> # <https://github.com/jakubroztocil/httpie/issues/802>
# noinspection PyUnresolvedReferences
orig = http.client._MAXHEADERS orig = http.client._MAXHEADERS
http.client._MAXHEADERS = limit or float('Inf') http.client._MAXHEADERS = limit or float('Inf')
try: try:
@ -145,29 +152,23 @@ def compress_body(request: requests.PreparedRequest, always: bool):
request.headers['Content-Length'] = str(len(deflated_data)) 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( def build_requests_session(
verify: bool,
ssl_version: str = None, ssl_version: str = None,
ciphers: str = None,
) -> requests.Session: ) -> requests.Session:
requests_session = requests.Session() requests_session = requests.Session()
# Install our adapter. # Install our adapter.
requests_session.mount('https://', HTTPieHTTPSAdapter( https_adapter = HTTPieHTTPSAdapter(
ciphers=ciphers,
verify=verify,
ssl_version=( ssl_version=(
SSL_VERSION_ARG_MAPPING[ssl_version] AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
if ssl_version else None if ssl_version else None
) ),
)) )
requests_session.mount('https://', https_adapter)
# Install adapters from plugins. # Install adapters from plugins.
for plugin_cls in plugin_manager.get_transport_plugins(): for plugin_cls in plugin_manager.get_transport_plugins():

View File

@ -8,11 +8,54 @@ from httpie import __version__
from httpie.compat import is_windows from httpie.compat import is_windows
DEFAULT_CONFIG_DIR = Path(os.environ.get( ENV_XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
'HTTPIE_CONFIG_DIR', ENV_HTTPIE_CONFIG_DIR = 'HTTPIE_CONFIG_DIR'
os.path.expanduser('~/.httpie') if not is_windows else DEFAULT_CONFIG_DIRNAME = 'httpie'
os.path.expandvars(r'%APPDATA%\\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): class ConfigFileError(Exception):

View File

@ -13,10 +13,11 @@ from httpie.client import collect_messages
from httpie.context import Environment from httpie.context import Environment
from httpie.downloads import Downloader from httpie.downloads import Downloader
from httpie.output.writer import write_message, write_stream 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 from httpie.status import ExitStatus, http_status_to_exit_status
# noinspection PyDefaultArgument
def main( def main(
args: List[Union[str, bytes]] = sys.argv, args: List[Union[str, bytes]] = sys.argv,
env=Environment(), env=Environment(),

View File

@ -3,6 +3,10 @@ from httpie.plugins import FormatterPlugin
class HeadersFormatter(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: def format_headers(self, headers: str) -> str:
""" """
Sorts headers by name while retaining relative Sorts headers by name while retaining relative

View File

@ -4,11 +4,12 @@ import json
from httpie.plugins import FormatterPlugin from httpie.plugins import FormatterPlugin
DEFAULT_INDENT = 4
class JSONFormatter(FormatterPlugin): 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: def format_body(self, body: str, mime: str) -> str:
maybe_json = [ maybe_json = [
'json', 'json',
@ -26,8 +27,8 @@ class JSONFormatter(FormatterPlugin):
# unicode escapes to improve readability. # unicode escapes to improve readability.
body = json.dumps( body = json.dumps(
obj=obj, obj=obj,
sort_keys=True, sort_keys=self.format_options['json']['sort_keys'],
ensure_ascii=False, ensure_ascii=False,
indent=DEFAULT_INDENT indent=self.format_options['json']['indent']
) )
return body return body

View File

@ -1,7 +1,8 @@
import re import re
from typing import Optional, List 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 from httpie.context import Environment

View File

@ -152,6 +152,7 @@ def get_stream_type_and_kwargs(
groups=args.prettify, groups=args.prettify,
color_scheme=args.style, color_scheme=args.style,
explicit_json=args.json, explicit_json=args.json,
format_options=args.format_options,
) )
} }
else: else:

View File

@ -7,18 +7,3 @@ from httpie.plugins.base import (
AuthPlugin, FormatterPlugin, AuthPlugin, FormatterPlugin,
ConverterPlugin, TransportPlugin 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,
)

View File

@ -3,7 +3,7 @@ class BasePlugin:
# The name of the plugin, eg. "My auth". # The name of the plugin, eg. "My auth".
name = None 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. # under --auth-type.
description = None description = None
@ -15,7 +15,9 @@ class AuthPlugin(BasePlugin):
""" """
Base auth plugin class. 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` See also `test_auth_plugins.py`
@ -33,13 +35,22 @@ class AuthPlugin(BasePlugin):
# Set this to `False` to disable the parsing and error handling. # Set this to `False` to disable the parsing and error handling.
auth_parse = True auth_parse = True
# Set to `True` to make it possible for this auth
# plugin to acquire credentials from the users 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`, # If both `auth_parse` and `prompt_password` are set to `True`,
# and the value of `-a` lacks the password part, # and the value of `-a` lacks the password part,
# then the user will be prompted to type the password in. # then the user will be prompted to type the password in.
prompt_password = True prompt_password = True
# Will be set to the raw value of `-a` (if provided) before # 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 raw_auth = None
def get_auth(self, username=None, password=None): def get_auth(self, username=None, password=None):
@ -58,8 +69,13 @@ class AuthPlugin(BasePlugin):
class TransportPlugin(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): 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): def __init__(self, mime):
self.mime = mime self.mime = mime
@ -89,17 +113,22 @@ class ConverterPlugin(BasePlugin):
class FormatterPlugin(BasePlugin): class FormatterPlugin(BasePlugin):
"""
Possibly formats response body & headers for prettified terminal display.
"""
group_name = 'format' group_name = 'format'
def __init__(self, **kwargs): def __init__(self, **kwargs):
""" """
:param env: an class:`Environment` instance :param env: an class:`Environment` instance
:param kwargs: additional keyword argument that some :param kwargs: additional keyword argument that some
processor might require. formatters might require.
""" """
self.enabled = True self.enabled = True
self.kwargs = kwargs self.kwargs = kwargs
self.format_options = kwargs['format_options']
def format_headers(self, headers: str) -> str: def format_headers(self, headers: str) -> str:
"""Return processed `headers` """Return processed `headers`

View File

@ -22,6 +22,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
See https://github.com/jakubroztocil/httpie/issues/212 See https://github.com/jakubroztocil/httpie/issues/212
""" """
# noinspection PyTypeChecker
request.headers['Authorization'] = type(self).make_header( request.headers['Authorization'] = type(self).make_header(
self.username, self.password).encode('latin1') self.username, self.password).encode('latin1')
return request return request
@ -36,6 +37,7 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
class BasicAuthPlugin(BuiltinAuthPlugin): class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth' name = 'Basic HTTP auth'
auth_type = 'basic' auth_type = 'basic'
netrc_parse = True
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def get_auth(self, username: str, password: str) -> HTTPBasicAuth: def get_auth(self, username: str, password: str) -> HTTPBasicAuth:
@ -43,9 +45,9 @@ class BasicAuthPlugin(BuiltinAuthPlugin):
class DigestAuthPlugin(BuiltinAuthPlugin): class DigestAuthPlugin(BuiltinAuthPlugin):
name = 'Digest HTTP auth' name = 'Digest HTTP auth'
auth_type = 'digest' auth_type = 'digest'
netrc_parse = True
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
def get_auth( def get_auth(

View 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,
)

View File

@ -5,7 +5,7 @@ Persistent, JSON-serialized sessions.
import os import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Iterable, Optional, Union
from urllib.parse import urlsplit from urllib.parse import urlsplit
from requests.auth import AuthBase from requests.auth import AuthBase
@ -13,7 +13,7 @@ from requests.cookies import RequestsCookieJar, create_cookie
from httpie.cli.dicts import RequestHeadersDict from httpie.cli.dicts import RequestHeadersDict
from httpie.config import BaseConfigDict, DEFAULT_CONFIG_DIR 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' SESSIONS_DIR_NAME = 'sessions'
@ -144,3 +144,8 @@ class Session(BaseConfigDict):
def auth(self, auth: dict): def auth(self, auth: dict):
assert {'type', 'raw_auth'} == auth.keys() assert {'type', 'raw_auth'} == auth.keys()
self['auth'] = auth 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
View 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 its also passed to the connection
# in `super().cert_verify()`.
cert_reqs=ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
)

View File

@ -1,8 +1,12 @@
from __future__ import division from __future__ import division
import json import json
import mimetypes import mimetypes
import time
from collections import OrderedDict from collections import OrderedDict
from http.cookiejar import parse_ns_headers
from pprint import pformat from pprint import pformat
from typing import List, Tuple
import requests.auth import requests.auth
@ -83,3 +87,27 @@ def get_content_type(filename):
if encoding: if encoding:
content_type = '%s; charset=%s' % (mime, encoding) content_type = '%s; charset=%s' % (mime, encoding)
return content_type 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
]

View File

@ -4,6 +4,7 @@
[tool:pytest] [tool:pytest]
# <https://docs.pytest.org/en/latest/customize.html> # <https://docs.pytest.org/en/latest/customize.html>
norecursedirs = tests/fixtures norecursedirs = tests/fixtures
addopts = --tb=native
[pycodestyle] [pycodestyle]

View File

@ -3,6 +3,7 @@ import mock
import pytest import pytest
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from httpie.status import ExitStatus
from httpie.utils import ExplicitNullAuth from httpie.utils import ExplicitNullAuth
from utils import http, add_auth, HTTP_OK, MockEnvironment from utils import http, add_auth, HTTP_OK, MockEnvironment
import httpie.cli.constants import httpie.cli.constants
@ -78,6 +79,8 @@ def test_missing_auth(httpbin):
def test_netrc(httpbin_both): def test_netrc(httpbin_both):
# This one gets handled by requests (no --auth, --auth-type present),
# thats why we patch inside `requests.sessions`.
with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth: with mock.patch('requests.sessions.get_netrc_auth') as get_netrc_auth:
get_netrc_auth.return_value = ('httpie', 'password') get_netrc_auth.return_value = ('httpie', 'password')
r = http(httpbin_both + '/basic-auth/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): 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') get_netrc_auth.return_value = ('httpie', 'password')
r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password') r = http('--ignore-netrc', httpbin_both + '/basic-auth/httpie/password')
assert get_netrc_auth.call_count == 0 assert get_netrc_auth.call_count == 0
assert 'HTTP/1.1 401 UNAUTHORIZED' in r 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(): def test_ignore_netrc_together_with_auth():
args = httpie.cli.definition.parser.parse_args( args = httpie.cli.definition.parser.parse_args(
args=['--ignore-netrc', '--auth=username:password', 'example.org'], args=['--ignore-netrc', '--auth=username:password', 'example.org'],
env=MockEnvironment(), env=MockEnvironment(),
) )
assert isinstance(args.auth, HTTPBasicAuth) 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)

View File

@ -1,9 +1,11 @@
from mock import mock from mock import mock
from httpie.cli.constants import SEPARATOR_CREDENTIALS 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 from utils import http, HTTP_OK
# TODO: run all these tests in session mode as well # TODO: run all these tests in session mode as well
USERNAME = 'user' USERNAME = 'user'

View File

@ -1,7 +1,14 @@
from pathlib import Path
import pytest import pytest
from _pytest.monkeypatch import MonkeyPatch
from httpie.compat import is_windows 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 from utils import HTTP_OK, MockEnvironment, http
@ -48,3 +55,50 @@ def test_default_options_overwrite(httpbin):
assert r.json['json'] == { assert r.json['json'] == {
"foo": "bar" "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

View File

@ -1,12 +1,19 @@
import argparse
import json
import os import os
from tempfile import gettempdir from tempfile import gettempdir
from urllib.request import urlopen from urllib.request import urlopen
import pytest import pytest
from utils import MockEnvironment, http, HTTP_OK, COLOR, CRLF from httpie.cli.argtypes import (
from httpie.status import ExitStatus PARSED_DEFAULT_FORMAT_OPTIONS,
parse_format_options,
)
from httpie.cli.definition import parser
from httpie.output.formatters.colors import get_lexer 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]) @pytest.mark.parametrize('stdout_isatty', [True, False])
@ -58,19 +65,19 @@ class TestColors:
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'], argnames=['mime', 'explicit_json', 'body', 'expected_lexer_name'],
argvalues=[ argvalues=[
('application/json', False, None, 'JSON'), ('application/json', False, None, 'JSON'),
('application/json+foo', False, None, 'JSON'), ('application/json+foo', False, None, 'JSON'),
('application/foo+json', False, None, 'JSON'), ('application/foo+json', False, None, 'JSON'),
('application/json-foo', False, None, 'JSON'), ('application/json-foo', False, None, 'JSON'),
('application/x-json', False, None, 'JSON'), ('application/x-json', False, None, 'JSON'),
('foo/json', False, None, 'JSON'), ('foo/json', False, None, 'JSON'),
('foo/json+bar', False, None, 'JSON'), ('foo/json+bar', False, None, 'JSON'),
('foo/bar+json', False, None, 'JSON'), ('foo/bar+json', False, None, 'JSON'),
('foo/json-foo', False, None, 'JSON'), ('foo/json-foo', False, None, 'JSON'),
('foo/x-json', False, None, 'JSON'), ('foo/x-json', False, None, 'JSON'),
('application/vnd.comverge.grid+hal+json', False, None, 'JSON'), ('application/vnd.comverge.grid+hal+json', False, None, 'JSON'),
('text/plain', True, '{}', 'JSON'), ('text/plain', True, '{}', 'JSON'),
('text/plain', True, 'foo', 'Text only'), ('text/plain', True, 'foo', 'Text only'),
] ]
) )
def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name): def test_get_lexer(self, mime, explicit_json, body, expected_lexer_name):
@ -83,7 +90,7 @@ class TestColors:
class TestPrettyOptions: class TestPrettyOptions:
"""Test the --pretty flag handling.""" """Test the --pretty handling."""
def test_pretty_enabled_by_default(self, httpbin): def test_pretty_enabled_by_default(self, httpbin):
env = MockEnvironment(colors=256) env = MockEnvironment(colors=256)
@ -138,6 +145,7 @@ class TestLineEndings:
and as the headers/body separator. and as the headers/body separator.
""" """
def _validate_crlf(self, msg): def _validate_crlf(self, msg):
lines = iter(msg.splitlines(True)) lines = iter(msg.splitlines(True))
for header in lines: for header in lines:
@ -171,3 +179,199 @@ class TestLineEndings:
def test_CRLF_formatted_request(self, httpbin): def test_CRLF_formatted_request(self, httpbin):
r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get') r = http('--pretty=format', '--print=HB', 'GET', httpbin.url + '/get')
self._validate_crlf(r) 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

View File

@ -1,14 +1,17 @@
# coding=utf-8 # coding=utf-8
import json
import os import os
import shutil import shutil
import sys from datetime import datetime
from tempfile import gettempdir from tempfile import gettempdir
import pytest import pytest
from httpie.plugins.builtin import HTTPBasicAuth
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK
from fixtures import UNICODE 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: class SessionTestBase:
@ -186,3 +189,91 @@ class TestSession(SessionTestBase):
httpbin.url + '/get', env=self.env()) httpbin.url + '/get', env=self.env())
finally: finally:
os.chdir(cwd) 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

View File

@ -1,11 +1,9 @@
import os
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
import requests.exceptions import requests.exceptions
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
from httpie.status import ExitStatus from httpie.status import ExitStatus
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
from utils import HTTP_OK, TESTS_ROOT, http from utils import HTTP_OK, TESTS_ROOT, http
@ -23,10 +21,12 @@ except ImportError:
requests.exceptions.SSLError, 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: # FIXME:
# We test against a local httpbin instance which uses a self-signed cert. # We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error. # 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() 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): def test_ssl_version(httpbin_secure, ssl_version):
try: try:
r = http( r = http(
@ -113,3 +114,23 @@ class TestServerCert:
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure): def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
with pytest.raises(ssl_errors): with pytest.raises(ssl_errors):
http(httpbin_secure.url + '/get', '--verify', __file__) 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

View File

@ -17,27 +17,37 @@ class TestMultipartFormDataFileUpload:
def test_upload_ok(self, httpbin): def test_upload_ok(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', 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 HTTP_OK in r
assert 'Content-Disposition: form-data; name="foo"' in r assert 'Content-Disposition: form-data; name="foo"' in r
assert 'Content-Disposition: form-data; name="test-file";' \ 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 FILE_CONTENT in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
assert 'Content-Type: text/plain' in r assert 'Content-Type: text/plain' in r
def test_upload_multiple_fields_with_the_same_name(self, httpbin): def test_upload_multiple_fields_with_the_same_name(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
'test-file@%s' % FILE_PATH_ARG, f'test-file@{FILE_PATH_ARG}',
'test-file@%s' % FILE_PATH_ARG) f'test-file@{FILE_PATH_ARG}')
assert HTTP_OK in r assert HTTP_OK in r
assert r.count('Content-Disposition: form-data; name="test-file";' 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 # Should be 4, but is 3 because httpbin
# doesn't seem to support filed field lists # doesn't seem to support filed field lists
assert r.count(FILE_CONTENT) in [3, 4] assert r.count(FILE_CONTENT) in [3, 4]
assert r.count('Content-Type: text/plain') == 2 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: class TestRequestBodyFromFilePath:
""" """

View File

@ -216,6 +216,7 @@ def http(
add_to_args.append('--timeout=3') add_to_args.append('--timeout=3')
complete_args = [program_name, *add_to_args, *args] complete_args = [program_name, *add_to_args, *args]
# print(' '.join(complete_args))
def dump_stderr(): def dump_stderr():
stderr.seek(0) stderr.seek(0)