Compare commits

...

66 Commits
2.2.0 ... 2.3.0

Author SHA1 Message Date
dc3687f7ac v2.3.0 2020-10-25 20:39:01 +01:00
51bc8fb2c6 Update setup.py 2020-10-03 12:01:36 +02:00
a69d6f44fd New Twitter handle — @httpie 2020-10-03 11:01:08 +02:00
507cd6e255 Fix table formatting
reStructuredText is a mess
2020-09-29 21:26:38 +02:00
759e4400d0 Cleanup 2020-09-28 17:02:22 +02:00
8cb1af7376 Cleanup 2020-09-28 16:58:59 +02:00
2f8d330b57 Fix --offline --chunked, add more tests 2020-09-28 16:40:16 +02:00
32d8b481e9 Fix --offline --multipart, add more tests 2020-09-28 16:22:34 +02:00
75f1e02215 README 2020-09-28 12:55:39 +02:00
70ba84dc48 Fix fixture encoding on Windows 2020-09-28 12:53:28 +02:00
5a5b42340f PEP8 2020-09-28 12:50:45 +02:00
299250b3c3 Merge branch 'feature/uploads2020' 2020-09-28 12:43:09 +02:00
6925d930da Add support for streamed uploads, --chunked, finish --multipart, etc.
Close #201
Close #753
Close #684
Close #903
Related: #452
2020-09-28 12:16:57 +02:00
c1948f8340 Update README.rst 2020-09-25 22:27:54 +02:00
b80ba040ac Update README.rst 2020-09-25 22:26:39 +02:00
b7754f92ce Merge branch 'master' into feature/uploads2020
# Conflicts:
#	httpie/cli/argparser.py
#	httpie/uploads.py
2020-09-25 14:46:19 +02:00
e4e40e5b06 Request content type 2020-09-25 14:44:22 +02:00
d12af4a569 WIP 2020-09-25 13:44:28 +02:00
c431ed7728 CHANGELOG
#963
2020-09-20 09:30:18 +02:00
16ef08a159 Gracefully ignore cookie expiry dates in invalid format
Close #963
2020-09-20 09:21:10 +02:00
100872b5cf pep8 2020-08-19 10:39:13 +02:00
664cebfbcc Update README.rst 2020-08-19 10:31:20 +02:00
743f9738a3 Update README.rst 2020-08-19 10:30:40 +02:00
69445c106c Cleanup 2020-08-19 10:25:47 +02:00
1813cf6156 Add --multipart and --boundary 2020-08-19 10:22:50 +02:00
a23b0e39e5 Update README.rst 2020-08-17 13:34:23 +02:00
06dec4e6c6 Update README.rst 2020-08-17 13:31:51 +02:00
ce185bd0fa Update __init__.py 2020-08-17 13:16:57 +02:00
1e1dbfeba0 Update README.rst 2020-08-17 13:14:52 +02:00
5a908aa411 pep8 2020-08-15 17:51:43 +02:00
6cd934d1b8 Add support for multipart upload streaming
Close #684, #201
2020-08-15 17:50:00 +02:00
d32c8cab12 Syntax 2020-08-15 15:34:31 +02:00
5ce7c190e9 Add a --quiet example 2020-08-15 15:33:24 +02:00
1aa1366f99 Finish --quiet 2020-08-15 15:26:29 +02:00
2c7f24e3e5 Added additional tests to verify downloads work properly with quiet flag 2020-08-15 15:26:29 +02:00
c90d039a0b fixed issues related to downloading and using quiet at the same time 2020-08-15 15:26:29 +02:00
ae22d4e754 Additional Aesthetic changes 2020-08-15 15:26:29 +02:00
69e1067a2c Aesthetic changes 2020-08-15 15:26:29 +02:00
7e38f9ccf0 Added additional tests for flag and better documentation 2020-08-15 15:26:29 +02:00
d546081340 Solved issue pertaining to downloads and added additional testing functionality for devnull 2020-08-15 15:26:29 +02:00
6421c145d9 Added changes suggested in the PR review 2020-08-15 15:26:29 +02:00
61e7cd786e Added a documentation entry and modified CHANGELOG 2020-08-15 15:26:29 +02:00
4bd2e622a5 Added tests for --quiet flag 2020-08-15 15:26:29 +02:00
a4a1e8d43b Added a quiet functionality 2020-08-15 15:26:28 +02:00
ebf2139fd5 Introduce CurliPie to convert from cURL to HTTPie (#843) 2020-08-14 15:27:49 +02:00
6c84cebed4 Update build.yml 2020-08-06 22:35:35 +02:00
10246366da Quieten ssl tests (#952)
* Add skip when required TLS version unsupported

Allow tests to skip, rather than fail from SSL errors with unsupported
TLS version, e.g. if Openssl is configured with MinProtocol higher than
the version being tested.

* Regenerate test certificate and key

Regenerate these with more secure settings for the sake of future
proofing, regenerate the key using RSA 4096 and sign the certificate
with SHA512.

This fixes test failures in tests/test_ssl.py when the user's OpenSSL
security level is set to a value greater than 1 and resolves issue #948

* Suppress SSL warnings in no verify tests
2020-08-06 22:24:03 +02:00
a448b0d928 Fix minor typos in the README and CONTRIBUTING docs (#956)
* Fix typo in CONTRIBUTING docs

* Fix minor typo in README (exists --> exits)
2020-07-16 22:47:39 +02:00
0541490dda Add test to test auth plugin reused in session (#938)
* Add test to test auth plugin reused in session

* Remove unnecessary assertion in auth-plugin test

* Fixed auth test to use same session file

* Add test for password prompt behaviour in session

* Edit auth readme for plugin clarity
2020-07-10 12:48:26 +02:00
3704db9b6d Install setuptools & wheel in CI II. 2020-07-07 13:39:02 +02:00
d1665b08d2 Install setuptools & wheel in CI 2020-07-07 13:38:27 +02:00
1a4e0c2646 Add wheel to tests_require
Speculative. Not sure why it’s suddenly needed.

https://github.com/jakubroztocil/httpie/runs/845338452
2020-07-07 13:35:09 +02:00
0d480139e4 Split session JSON serialization and writing to file
To avoid writing invalid JSON in case of presence of unserializable data due to an internal bug.
2020-07-07 13:26:47 +02:00
9931747901 Update CONTRIBUTING.rst 2020-07-07 13:19:42 +02:00
8891afa3b7 Reorganize and expand CONTRIBUTING.rst 2020-06-26 17:48:04 +02:00
4f493d51f8 Add Windows setup instructions (#941)
* Added a sub-section specifically for windows

* Wrote instructions for creating/activating venv

* Split the PS and CMD examples into two separate code blocks

* Specified language for the code blocks

* Added test instructions for windows

* Converted slash to backward
2020-06-26 17:25:26 +02:00
cf937b6b79 Remove tox (#944)
* Removed the instructions of tox testing

* Deleted tox.ini

* removed tox from requirements

* removed tox from setup.cfg

* removed tox from the Makefile

* removed tox from contributing docs

* updated the CHANGELOG

* removed tox from .gitignore
2020-06-26 17:22:06 +02:00
14677bd25d Cleanup inline to-dos
I.
2020-06-25 11:36:09 +02:00
49e71d252f Fixed test_ciphers_none_can_be_selected on OpenBSD
Thanks @juped!
2020-06-19 18:26:08 +02:00
d6f25b1017 Update Brew formula for v2.2.0 2020-06-19 02:23:12 +02:00
a434cddd42 Fix install_requires 2020-06-19 01:13:11 +02:00
55d7af86fd Install requests[socks] by default for out of the box SOCKS support
Close #904
2020-06-19 00:56:30 +02:00
978aace86c Update README.rst (#737) 2020-06-19 00:25:29 +02:00
ecdeffe7c8 CHANGELOG + README 2020-06-18 23:23:10 +02:00
9500ce136a Combine cookies from original request and session file
Close #932
Co-authored-by: kbanc <katherine.bancoft@gmail.com>
Co-authored-by: Gabriel Cruz <gabs.oficial98@gmail.com>
2020-06-18 23:17:33 +02:00
93d07cfe57 v2.3.0-dev 2020-06-18 22:25:07 +02:00
48 changed files with 1677 additions and 566 deletions

2
.github/FUNDING.yml vendored
View File

@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username otechie: # Replace with a single Otechie username
custom: https://paypal.me/roztocil custom:

View File

@ -9,7 +9,7 @@ jobs:
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: 3.8 python-version: 3.8
- run: pip install --upgrade pip - run: python -m pip install --upgrade pip setuptools wheel
- run: make install - run: make install
- run: make pycodestyle - run: make pycodestyle
- run: make test-cover - run: make test-cover
@ -32,6 +32,6 @@ jobs:
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip - run: python -m pip install --upgrade pip setuptools wheel
- run: pip install --upgrade --editable . - run: python -m pip install --upgrade --editable .
- run: python setup.py test - run: python setup.py test

1
.gitignore vendored
View File

@ -52,7 +52,6 @@ pip-delete-this-directory.txt
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
.tox/
.nox/ .nox/
.coverage .coverage
.coverage.* .coverage.*

View File

@ -6,6 +6,21 @@ 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.3.0`_ (2020-10-25)
-------------------------
* Added support for multipart upload streaming (`#684`_).
* Added support for body-from-file upload streaming (``http httpbin.org/post @file``).
* Added ``--chunked`` to allow chunked transfer encoding.
* Added ``--multipart`` to allow ``multipart/form-data`` encoding for non-file ``--form`` requests as well.
* Added ``--boundary`` to allow a custom boundary string for ``multipart/form-data`` requests.
* Added support for combining cookies specified on the CLI and in a session file (`#932`_).
* Added out of the box SOCKS support with no extra installation (`#904`_).
* Added ``--quiet, -q`` flag to enforce silent behaviour.
* Fixed the handling of invalid ``expires`` dates in ``Set-Cookie`` headers (`#963`_).
* Removed Tox testing entirely (`#943`_).
`2.2.0`_ (2020-06-18) `2.2.0`_ (2020-06-18)
------------------------- -------------------------
@ -437,11 +452,13 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
.. _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 .. _2.2.0: https://github.com/jakubroztocil/httpie/compare/2.1.0...2.2.0
.. _2.3.0: https://github.com/jakubroztocil/httpie/compare/2.2.0...2.3.0
.. _#128: https://github.com/jakubroztocil/httpie/issues/128 .. _#128: https://github.com/jakubroztocil/httpie/issues/128
.. _#488: https://github.com/jakubroztocil/httpie/issues/488 .. _#488: https://github.com/jakubroztocil/httpie/issues/488
.. _#668: https://github.com/jakubroztocil/httpie/issues/668 .. _#668: https://github.com/jakubroztocil/httpie/issues/668
.. _#684: https://github.com/jakubroztocil/httpie/issues/684
.. _#718: https://github.com/jakubroztocil/httpie/issues/718 .. _#718: https://github.com/jakubroztocil/httpie/issues/718
.. _#719: https://github.com/jakubroztocil/httpie/issues/719 .. _#719: https://github.com/jakubroztocil/httpie/issues/719
.. _#840: https://github.com/jakubroztocil/httpie/issues/840 .. _#840: https://github.com/jakubroztocil/httpie/issues/840
@ -450,5 +467,9 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
.. _#870: https://github.com/jakubroztocil/httpie/issues/870 .. _#870: https://github.com/jakubroztocil/httpie/issues/870
.. _#895: https://github.com/jakubroztocil/httpie/issues/895 .. _#895: https://github.com/jakubroztocil/httpie/issues/895
.. _#920: https://github.com/jakubroztocil/httpie/issues/920 .. _#920: https://github.com/jakubroztocil/httpie/issues/920
.. _#904: https://github.com/jakubroztocil/httpie/issues/904
.. _#925: https://github.com/jakubroztocil/httpie/issues/925 .. _#925: https://github.com/jakubroztocil/httpie/issues/925
.. _#932: https://github.com/jakubroztocil/httpie/issues/932
.. _#934: https://github.com/jakubroztocil/httpie/issues/934 .. _#934: https://github.com/jakubroztocil/httpie/issues/934
.. _#943: https://github.com/jakubroztocil/httpie/issues/943
.. _#963: https://github.com/jakubroztocil/httpie/issues/963

View File

@ -16,17 +16,34 @@ to your bug report, e.g.:
.. code-block:: bash .. code-block:: bash
$ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR] $ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
[COMPLETE OUTPUT]
<COMPLETE OUTPUT>
2. Contributing Code and Docs 2. Contributing Code and Docs
============================= =============================
Before working on a new feature or a bug, please browse `existing issues`_ Before working on a new feature or a bug, please browse `existing issues`_
to see whether it has been previously discussed. If the change in question to see whether it has previously been discussed.
is a bigger one, it's always good to discuss before you start working on
it. If your change alters HTTPies behaviour or interface, it's a good idea to
discuss it before you start working on it.
If you are fixing an issue, the first step should be to create a test case that
reproduces the incorrect behaviour. That will also help you to build an
understanding of the issue at hand.
**Pull requests introducing code changes without tests
will generally not get merged. The same goes for PRs changing HTTPies
behaviour and not providing documentation.**
Conversely, PRs consisting of documentation improvements or tests
for existing-yet-previously-untested behavior will very likely be merged.
Therefore, docs and tests improvements are a great candidate for your first
contribution.
Consider also adding a ``CHANGELOG`` entry for your changes.
Development Environment Development Environment
@ -87,7 +104,7 @@ activate the environment — we have created a symlink for you. Its a bit of
a hack but it works™.) a hack but it works™.)
You should now see ``(httpie)`` next to your shell prompt, and You should now see ``(httpie)`` next to your shell prompt, and
the ``http`` should point to you development copy: the ``http`` command should point to your development copy:
.. code-block:: .. code-block::
@ -123,8 +140,7 @@ so please make sure all checks pass.
Running tests locally Running tests locally
********************* *********************
HTTPie uses the `pytest`_ runner. It also uses `Tox`_ which allows you to run HTTPie uses the `pytest`_ runner.
tests on multiple Python versions even when testing locally.
.. code-block:: bash .. code-block:: bash
@ -135,9 +151,6 @@ tests on multiple Python versions even when testing locally.
# Run tests with coverage # Run tests with coverage
make test-cover make test-cover
# Run all tests in all of the supported and available Pythons via Tox
make test-tox
# Test PEP8 compliance # Test PEP8 compliance
make pycodestyle make pycodestyle
@ -158,22 +171,65 @@ can run specific tests from the terminal:
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload py.test tests/test_uploads.py::TestMultipartFormDataFileUpload
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
# Run specific tests on the on all Pythons via Tox
# (change to `tox -e py37' to limit Python version)
tox -- tests/test_uploads.py --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
----- -----
See `Makefile`_ for additional development utilities. See `Makefile`_ for additional development utilities.
Windows
*******
Finally, don't forget to add yourself to `AUTHORS`_! If you are on a Windows machine and not able to run ``make``,
follow the next steps for a basic setup. As a prerequisite, you need to have
Python 3.6+ installed.
Create a virtual environment and activate it:
.. code-block:: powershell
python -m venv --prompt httpie venv
venv\Scripts\activate
Install HTTPie in editable mode with all the dependencies:
.. code-block:: powershell
pip install --upgrade -e . -r requirements-dev.txt
You should now see ``(httpie)`` next to your shell prompt, and
the ``http`` command should point to your development copy:
.. code-block:: powershell
# In PowerShell:
(httpie) PS C:\Users\ovezovs\httpie> Get-Command http
CommandType Name Version Source
----------- ---- ------- ------
Application http.exe 0.0.0.0 C:\Users\ovezovs\httpie\venv\Scripts\http.exe
.. code-block:: bash
# In CMD:
(httpie) C:\Users\ovezovs\httpie> where http
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
(httpie) C:\Users\ovezovs\httpie> http --version
2.3.0-dev
Use ``pytest`` to run tests locally with an active virtual environment:
.. code-block:: bash
# Run all tests
py.test
-----
Finally, feel free to add yourself to `AUTHORS`_!
.. _Tox: http://tox.testrun.org
.. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini
.. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open .. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open
.. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst .. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst
.. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile .. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile

View File

@ -38,7 +38,7 @@ clean:
rm -rf $(VENV_ROOT) rm -rf $(VENV_ROOT)
# Remove symlink for virtualenvwrapper, if weve created one. # Remove symlink for virtualenvwrapper, if weve created one.
[ -n "$(WORKON_HOME)" -a -L "$(WORKON_HOME)/httpie" -a -f "$(WORKON_HOME)/httpie" ] && rm $(WORKON_HOME)/httpie || true [ -n "$(WORKON_HOME)" -a -L "$(WORKON_HOME)/httpie" -a -f "$(WORKON_HOME)/httpie" ] && rm $(WORKON_HOME)/httpie || true
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info rm -rf *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
find . -name '__pycache__' -delete -o -name '*.pyc' -delete find . -name '__pycache__' -delete -o -name '*.pyc' -delete
@echo @echo
@ -86,7 +86,7 @@ test-cover: test
# test-all is meant to test everything — even this Makefile # test-all is meant to test everything — even this Makefile
test-all: clean install test test-tox test-dist pycodestyle test-all: clean install test test-dist pycodestyle
@echo @echo
@ -94,12 +94,6 @@ test-dist: test-sdist test-bdist-wheel
@echo @echo
test-tox: uninstall-httpie install
@echo $(H1)Running tests on all Pythons via Tox$(H1END)
$(VENV_BIN)/tox
@echo
test-sdist: clean venv test-sdist: clean venv
@echo $(H1)Testing sdist build an installation$(H1END) @echo $(H1)Testing sdist build an installation$(H1END)
$(VENV_PYTHON) setup.py sdist $(VENV_PYTHON) setup.py sdist
@ -170,7 +164,6 @@ uninstall-httpie:
############################################################################### ###############################################################################
pdf: pdf:
# NOTE: rst2pdf needs to be installed manually and against a Python 2
@echo "Converting README.rst to PDF…" @echo "Converting README.rst to PDF…"
rst2pdf \ rst2pdf \
--strip-elements-with-class=no-pdf \ --strip-elements-with-class=no-pdf \

View File

@ -1,12 +1,12 @@
HTTPie: a CLI, cURL-like tool for humans HTTPie: human-friendly CLI HTTP client for the API era
######################################## ######################################################
HTTPie (pronounced *aitch-tee-tee-pie*) is a command-line HTTP client.
Its goal is to make CLI interaction with web services as human-friendly as possible.
HTTPie is designed for testing, debugging, and generally interacting with APIs & HTTP servers.
The ``http`` & ``https`` commands allow for creating and sending arbitrary HTTP requests.
They use simple and natural syntax and provide formatted and colorized output.
HTTPie (pronounced *aitch-tee-tee-pie*) is a command line HTTP client.
Its goal is to make CLI interaction with web services as human-friendly
as possible. It provides a simple ``http`` command that allows for sending
arbitrary HTTP requests using a simple and natural syntax, and displays
colorized output. HTTPie can be used for testing, debugging, and
generally interacting with HTTP servers.
.. class:: no-web no-pdf .. class:: no-web no-pdf
@ -16,7 +16,7 @@ generally interacting with HTTP servers.
.. class:: no-web no-pdf .. class:: no-web no-pdf
.. image:: https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.gif .. image:: https://raw.githubusercontent.com/httpie/httpie/master/httpie.gif
:alt: HTTPie in action :alt: HTTPie in action
:width: 100% :width: 100%
:align: center :align: center
@ -35,7 +35,7 @@ where you can select your corresponding HTTPie version as well as run examples d
browser using a `termible.io <https://termible.io?utm_source=httpie-readme>`_ embedded terminal. browser using a `termible.io <https://termible.io?utm_source=httpie-readme>`_ embedded terminal.
If you are reading this on GitHub, then this text covers the current *development* version. If you are reading this on GitHub, then this text covers the current *development* version.
You are invited to submit fixes and improvements to the the docs by editing You are invited to submit fixes and improvements to the the docs by editing
`README.rst <https://github.com/jakubroztocil/httpie/blob/master/README.rst>`_. `README.rst <https://github.com/httpie/httpie/blob/master/README.rst>`_.
Main features Main features
@ -58,7 +58,7 @@ Main features
.. class:: no-web .. class:: no-web
.. image:: https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.png .. image:: https://raw.githubusercontent.com/httpie/httpie/master/httpie.png
:alt: HTTPie compared to cURL :alt: HTTPie compared to cURL
:width: 100% :width: 100%
:align: center :align: center
@ -107,6 +107,11 @@ system package manager, for example:
# CentOS, RHEL, ... # CentOS, RHEL, ...
$ yum install httpie $ yum install httpie
.. code-block:: bash
# Gentoo
$ emerge httpie
.. code-block:: bash .. code-block:: bash
# Arch Linux # Arch Linux
@ -162,11 +167,11 @@ Otherwise with ``pip``:
.. code-block:: bash .. code-block:: bash
$ pip install --upgrade https://github.com/jakubroztocil/httpie/archive/master.tar.gz $ pip install --upgrade https://github.com/httpie/httpie/archive/master.tar.gz
Verify that now we have the Verify that now we have the
`current development version identifier <https://github.com/jakubroztocil/httpie/blob/master/httpie/__init__.py#L6>`_ `current development version identifier <https://github.com/httpie/httpie/blob/master/httpie/__init__.py#L6>`_
with the ``-dev`` suffix, for example: with the ``-dev`` suffix, for example:
.. code-block:: bash .. code-block:: bash
@ -229,12 +234,12 @@ Build and print a request without sending it using `offline mode`_:
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/httpie/httpie/issues/83>`_
with `authentication`_: with `authentication`_:
.. code-block:: bash .. code-block:: bash
$ http -a USERNAME POST https://api.github.com/repos/jakubroztocil/httpie/issues/83/comments body='HTTPie is awesome! :heart:' $ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
Upload a file using `redirected input`_: Upload a file using `redirected input`_:
@ -467,8 +472,9 @@ their type is distinguished only by the separator used:
| | The ``==`` separator is used. | | | The ``==`` separator is used. |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Data Fields | Request data fields to be serialized as a JSON | | Data Fields | Request data fields to be serialized as a JSON |
| ``field=value``, | object (default), or to be form-encoded | | ``field=value``, | object (default), to be form-encoded |
| ``field=@file.txt`` | (``--form, -f``). | | ``field=@file.txt`` | (with ``--form, -f``), or to be serialized as |
| | ``multipart/form-data`` (with ``--multipart``). |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Raw JSON fields | Useful when sending JSON and one or | | Raw JSON fields | Useful when sending JSON and one or |
| ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, | | ``field:=json``, | more fields need to be a ``Boolean``, ``Number``, |
@ -476,10 +482,12 @@ their type is distinguished only by the separator used:
| | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` | | | ``meals:='["ham","spam"]'`` or ``pies:=[1,2,3]`` |
| | (note the quotes). | | | (note the quotes). |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
| Form File Fields | Only available with ``--form, -f``. | | Fields upload fields | Only available with ``--form, -f`` and |
| ``field@/dir/file`` | For example ``screenshot@~/Pictures/img.png``. | | ``field@/dir/file`` | ``--multipart``. |
| | The presence of a file field results | | ``field@file;type`` | For example ``screenshot@~/Pictures/img.png``, or |
| | in a ``multipart/form-data`` request. | | | ``'cv@cv.txt;text/markdown'``. |
| | With ``--form``, the presence of a file field |
| | results in a ``--multipart`` request. |
+-----------------------+-----------------------------------------------------+ +-----------------------+-----------------------------------------------------+
@ -530,7 +538,6 @@ Simple example:
$ http PUT httpbin.org/put name=John email=john@example.org $ http PUT httpbin.org/put name=John email=john@example.org
.. code-block:: http .. code-block:: http
PUT / HTTP/1.1 PUT / HTTP/1.1
Accept: application/json, */*;q=0.5 Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate Accept-Encoding: gzip, deflate
@ -680,7 +687,7 @@ submitted:
<input type="file" name="cv" /> <input type="file" name="cv" />
</form> </form>
Note that ``@`` is used to simulate a file upload form field, whereas Please 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 When uploading files, their content type is inferred from the file name. You can manually
@ -690,6 +697,70 @@ override the inferred content type:
$ http -f POST httpbin.org/post name='John Smith' cv@'~/files/data.bin;type=application/pdf' $ http -f POST httpbin.org/post name='John Smith' cv@'~/files/data.bin;type=application/pdf'
To perform a ``multipart/form-data`` request even without any files, use
``--multipart`` instead of ``--form``:
.. code-block:: bash
$ http --multipart --offline example.org hello=world
.. code-block:: http
POST / HTTP/1.1
Content-Length: 129
Content-Type: multipart/form-data; boundary=c31279ab254f40aeb06df32b433cbccb
Host: example.org
--c31279ab254f40aeb06df32b433cbccb
Content-Disposition: form-data; name="hello"
world
--c31279ab254f40aeb06df32b433cbccb--
File uploads are always streamed to avoid memory issues with large files.
By default, HTTPie uses a random unique string as the multipart boundary
but you can use ``--boundary`` to specify a custom string instead:
.. code-block:: bash
$ http --form --multipart --boundary=xoxo --offline example.org hello=world
.. code-block:: http
POST / HTTP/1.1
Content-Length: 129
Content-Type: multipart/form-data; boundary=xoxo
Host: example.org
--xoxo
Content-Disposition: form-data; name="hello"
world
--xoxo--
If you specify a custom ``Content-Type`` header without including the boundary
bit, HTTPie will add the boundary value (explicitly specified or auto-generated)
to the header automatically:
.. code-block:: bash
http --form --multipart --offline example.org hello=world Content-Type:multipart/letter
.. code-block:: http
POST / HTTP/1.1
Content-Length: 129
Content-Type: multipart/letter; boundary=c31279ab254f40aeb06df32b433cbccb
Host: example.org
--c31279ab254f40aeb06df32b433cbccb
Content-Disposition: form-data; name="hello"
world
--c31279ab254f40aeb06df32b433cbccb--
HTTP headers HTTP headers
============ ============
@ -771,7 +842,7 @@ Offline mode
Use ``--offline`` to construct HTTP requests without sending them anywhere. Use ``--offline`` to construct HTTP requests without sending them anywhere.
With ``--offline``, HTTPie builds a request based on the specified options and arguments, prints it to ``stdout``, With ``--offline``, HTTPie builds a request based on the specified options and arguments, prints it to ``stdout``,
and then exists. It works completely offline; no network connection is ever made. and then exits. It works completely offline; no network connection is ever made.
This has a number of use cases, including: This has a number of use cases, including:
@ -874,7 +945,7 @@ The currently supported authentication schemes are Basic and Digest
have higher priority). have higher priority).
``--auth-type, -A`` Specify the auth mechanism. Possible values are ``--auth-type, -A`` Specify the auth mechanism. Possible values are
``basic`` and ``digest``. The default value is ``basic``, ``digest``, or the name of any `auth plugins`_ you have installed. The default value is
``basic`` so it can often be omitted. ``basic`` so it can often be omitted.
=================== ====================================================== =================== ======================================================
@ -1051,15 +1122,6 @@ In your ``~/.bash_profile``:
SOCKS SOCKS
----- -----
Homebrew-installed HTTPie comes with SOCKS proxy support out of the box.
To enable SOCKS proxy support for non-Homebrew installations, youll
might need to install ``requests[socks]`` manually using ``pip``:
.. code-block:: bash
$ pip install -U requests[socks]
Usage is the same as for other types of `proxies`_: Usage is the same as for other types of `proxies`_:
.. code-block:: bash .. code-block:: bash
@ -1161,8 +1223,35 @@ be printed via several options:
``--verbose, -v`` Print the whole HTTP exchange (request and response). ``--verbose, -v`` Print the whole HTTP exchange (request and response).
This option also enables ``--all`` (see below). This option also enables ``--all`` (see below).
``--print, -p`` Selects parts of the HTTP exchange. ``--print, -p`` Selects parts of the HTTP exchange.
``--quiet, -q`` Don't print anything to ``stdout`` and ``stderr``.
================= ===================================================== ================= =====================================================
What parts of the HTTP exchange should be printed
-------------------------------------------------
All the other `output options`_ are under the hood just shortcuts for
the more powerful ``--print, -p``. It accepts a string of characters each
of which represents a specific part of the HTTP exchange:
========== ==================
Character Stands for
========== ==================
``H`` request headers
``B`` request body
``h`` response headers
``b`` response body
========== ==================
Print request and response headers:
.. code-block:: bash
$ http --print=Hh PUT httpbin.org/put hello=world
Verbose output
--------------
``--verbose`` can often be useful for debugging the request and generating ``--verbose`` can often be useful for debugging the request and generating
documentation examples: documentation examples:
@ -1192,28 +1281,17 @@ documentation examples:
[] []
} }
Quiet output
------------
What parts of the HTTP exchange should be printed ``--quiet`` redirects all output that would otherwise go to ``stdout``
------------------------------------------------- and ``stderr`` (except for error messages) to ``/dev/null``.
This doesnt affect output to a file via ``--output`` or ``--download``.
All the other `output options`_ are under the hood just shortcuts for
the more powerful ``--print, -p``. It accepts a string of characters each
of which represents a specific part of the HTTP exchange:
========== ==================
Character Stands for
========== ==================
``H`` request headers
``B`` request body
``h`` response headers
``b`` response body
========== ==================
Print request and response headers:
.. code-block:: bash .. code-block:: bash
$ http --print=Hh PUT httpbin.org/put hello=world # There will be no output:
$ http --quiet httpbin.org/post enjoy='the silence'
Viewing intermediary requests/responses Viewing intermediary requests/responses
@ -1270,9 +1348,14 @@ Redirected Input
================ ================
The universal method for passing request data is through redirected ``stdin`` The universal method for passing request data is through redirected ``stdin``
(standard input)—piping. Such data is buffered and then with no further (standard input)—piping.
processing used as the request body. There are multiple useful ways to use
piping: By default, ``stdin`` data is buffered and then with no further processing
used as the request body. If you provide ``Content-Length``, then the request
body is streamed without buffering. You can also use ``--chunked`` to enable
streaming via `chunked transfer encoding`_.
There are multiple useful ways to use piping:
Redirect from a file: Redirect from a file:
@ -1306,7 +1389,7 @@ You can even pipe web services together using HTTPie:
.. code-block:: bash .. code-block:: bash
$ http GET https://api.github.com/repos/jakubroztocil/httpie | http POST httpbin.org/post $ http GET https://api.github.com/repos/httpie/httpie | http POST httpbin.org/post
You can use ``cat`` to enter multiline data on the terminal: You can use ``cat`` to enter multiline data on the terminal:
@ -1361,6 +1444,33 @@ verbatim contents of that XML file with ``Content-Type: application/xml``:
$ http PUT httpbin.org/put @files/data.xml $ http PUT httpbin.org/put @files/data.xml
File uploads are always streamed to avoid memory issues with large files.
Chunked transfer encoding
=========================
You can use the ``--chunked`` flag to instruct HTTPie to use
``Transfer-Encoding: chunked``:
.. code-block:: bash
$ http --chunked PUT httpbin.org/put hello=world
.. code-block:: bash
$ http --chunked --multipart PUT httpbin.org/put hello=world foo@files/data.xml
.. code-block:: bash
$ http --chunked httpbin.org/post @files/data.xml
.. code-block:: bash
$ cat files/data.xml | http --chunked httpbin.org/post
Terminal output Terminal output
=============== ===============
@ -1508,7 +1618,7 @@ is being saved to a file.
.. code-block:: bash .. code-block:: bash
$ http --download https://github.com/jakubroztocil/httpie/archive/master.tar.gz $ http --download https://github.com/httpie/httpie/archive/master.tar.gz
.. code-block:: http .. code-block:: http
@ -1550,7 +1660,7 @@ headers and progress are still shown in the terminal:
.. code-block:: bash .. code-block:: bash
$ http -d https://github.com/jakubroztocil/httpie/archive/master.tar.gz | tar zxf - $ http -d https://github.com/httpie/httpie/archive/master.tar.gz | tar zxf -
@ -1745,6 +1855,60 @@ exchange after it has been created, specify the session name via
# But it is not updated: # But it is not updated:
$ http --session-read-only=./ro-session.json httpbin.org/headers Custom-Header:new-value $ http --session-read-only=./ro-session.json httpbin.org/headers Custom-Header:new-value
Cookie Storage Behaviour
------------------------
**TL;DR:** Cookie storage priority: Server response > Command line request > Session file
To set a cookie within a Session there are three options:
1. Get a ``Set-Cookie`` header in a response from a server
.. code-block:: bash
$ http --session=./session.json httpbin.org/cookie/set?foo=bar
2. Set the cookie name and value through the command line as seen in `cookies`_
.. code-block:: bash
$ http --session=./session.json httpbin.org/headers Cookie:foo=bar
3. Manually set cookie parameters in the json file of the session
.. code-block:: json
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.org/doc#sessions",
"httpie": "2.2.0-dev"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": {
"foo": {
"expires": null,
"path": "/",
"secure": false,
"value": "bar"
}
}
}
Cookies will be set in the session file with the priority specified above. For example, a cookie
set through the command line will overwrite a cookie of the same name stored
in the session file. If the server returns a ``Set-Cookie`` header with a
cookie of the same name, the returned cookie will overwrite the preexisting cookie.
Expired cookies are never stored. If a cookie in a session file expires, it will be removed before
sending a new request. If the server expires an existing cookie, it will also be removed from the
session file.
Config Config
====== ======
@ -1930,7 +2094,7 @@ Please use the following support channels:
* `StackOverflow <https://stackoverflow.com>`_ * `StackOverflow <https://stackoverflow.com>`_
to ask questions (please make sure to use the to ask questions (please make sure to use the
`httpie <https://stackoverflow.com/questions/tagged/httpie>`_ tag). `httpie <https://stackoverflow.com/questions/tagged/httpie>`_ tag).
* Tweet directly to `@clihttp <https://twitter.com/clihttp>`_. * Tweet directly to `@httpie <https://twitter.com/httpie>`_.
* You can also tweet directly to `@jakubroztocil`_. * You can also tweet directly to `@jakubroztocil`_.
@ -1953,12 +2117,16 @@ HTTPie friends
HTTPie plays exceptionally well with the following tools: HTTPie plays exceptionally well with the following tools:
* `http-prompt <https://github.com/httpie/http-prompt>`_
— interactive shell for HTTPie featuring autocomplete
and command syntax highlighting
* `jq <https://stedolan.github.io/jq/>`_ * `jq <https://stedolan.github.io/jq/>`_
— CLI JSON processor that — CLI JSON processor that
works great in conjunction with HTTPie works great in conjunction with HTTPie
* `http-prompt <https://github.com/eliangcs/http-prompt>`_
— interactive shell for HTTPie featuring autocomplete Helpers to convert from other client tools:
and command syntax highlighting
* `CurliPie <https://curlipie.now.sh/>`_ help convert cURL command line to HTTPie command line.
Alternatives Alternatives
@ -1973,27 +2141,27 @@ Alternatives
Contributing Contributing
------------ ------------
See `CONTRIBUTING.rst <https://github.com/jakubroztocil/httpie/blob/master/CONTRIBUTING.rst>`_. See `CONTRIBUTING.rst <https://github.com/httpie/httpie/blob/master/CONTRIBUTING.rst>`_.
Change log Change log
---------- ----------
See `CHANGELOG <https://github.com/jakubroztocil/httpie/blob/master/CHANGELOG.rst>`_. See `CHANGELOG <https://github.com/httpie/httpie/blob/master/CHANGELOG.rst>`_.
Artwork Artwork
------- -------
* `Logo <https://github.com/claudiatd/httpie-artwork>`_ by `Cláudia Delgado <https://github.com/claudiatd>`_. * `Logo <https://github.com/claudiatd/httpie-artwork>`_ by `Cláudia Delgado <https://github.com/claudiatd>`_.
* `Animation <https://raw.githubusercontent.com/jakubroztocil/httpie/master/httpie.gif>`_ by `Allen Smith <https://github.com/loranallensmith>`_ of GitHub. * `Animation <https://raw.githubusercontent.com/httpie/httpie/master/httpie.gif>`_ by `Allen Smith <https://github.com/loranallensmith>`_ of GitHub.
Licence Licence
------- -------
BSD-3-Clause: `LICENSE <https://github.com/jakubroztocil/httpie/blob/master/LICENSE>`_. BSD-3-Clause: `LICENSE <https://github.com/httpie/httpie/blob/master/LICENSE>`_.
@ -2006,7 +2174,7 @@ 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/httpie/httpie/contributors
.. _Jakub Roztocil: https://roztocil.co .. _Jakub Roztocil: https://roztocil.co
.. _@jakubroztocil: https://twitter.com/jakubroztocil .. _@jakubroztocil: https://twitter.com/jakubroztocil
@ -2019,12 +2187,12 @@ have contributed.
: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
.. |coverage| image:: https://img.shields.io/codecov/c/github/jakubroztocil/httpie?style=flat-square .. |coverage| image:: https://img.shields.io/codecov/c/github/httpie/httpie?style=flat-square
:target: https://codecov.io/gh/jakubroztocil/httpie :target: https://codecov.io/gh/httpie/httpie
:alt: Test coverage :alt: Test coverage
.. |build| image:: https://github.com/jakubroztocil/httpie/workflows/Build/badge.svg .. |build| image:: https://github.com/httpie/httpie/workflows/Build/badge.svg
:target: https://github.com/jakubroztocil/httpie/actions :target: https://github.com/httpie/httpie/actions
:alt: Build status of the master branch on Mac/Linux/Windows :alt: Build status of the master branch on Mac/Linux/Windows
.. |gitter| image:: https://img.shields.io/gitter/room/jkbrzt/httpie.svg?style=flat-square .. |gitter| image:: https://img.shields.io/gitter/room/jkbrzt/httpie.svg?style=flat-square

View File

@ -16,6 +16,7 @@ PACKAGES = [
'httpie', 'httpie',
'Pygments', 'Pygments',
'requests', 'requests',
'requests-toolbelt',
'certifi', 'certifi',
'urllib3', 'urllib3',
'idna', 'idna',

View File

@ -9,15 +9,15 @@ 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/e2/79/da6aec7b4356e8b325561b987c940e5b1e4de1200a5c3db7c57a97d61ca1/httpie-2.1.0.tar.gz" url "https://files.pythonhosted.org/packages/37/6c/0d050f49e3b2bac589367d0c3aee9c078e23c6914b0210ffc0117218bdaf/httpie-2.2.0.tar.gz"
sha256 "a76f1c72e83bd03cde3478c5f345d5570fdb2967ed19d68d09518088640b9e8e" sha256 "31ac28088ee6a0b6f3ba7a53379000c4d1910c1708c9ff768f84b111c14405a0"
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 "1fb33d9c85dc462c2549a03cf08670edad8014a5fdf0a7cb26493c64af40283d" => :catalina sha256 "25f0e58f81a2cdd9cba772f07d67591533b4b31a2b970a356701aa046d4d9638" => :catalina
sha256 "a22030f0b96c698c90265286ee80ffbb03079d1d008a80c0bdb3ea15a17d3fbb" => :mojave sha256 "be158ebb4cfd327ebea02f7b8b8d63d093e474cd303eafff4a2b56b0611983a2" => :mojave
sha256 "9f994ecf826efe53a3a49d1c3193e271629068d11306df55adeea2842a8afb8c" => :high_sierra sha256 "f331edb94183bfc5fa9de4b4abf148cc91a3a8b3c0e24cc1f5e6b0a4172dd34d" => :high_sierra
end end
depends_on "python@3.8" depends_on "python@3.8"
@ -28,13 +28,13 @@ class Httpie < Formula
end end
resource "requests" do resource "requests" do
url "https://files.pythonhosted.org/packages/f5/4f/280162d4bd4d8aad241a21aecff7a6e46891b905a4341e7ab549ebaf7915/requests-2.23.0.tar.gz" url "https://files.pythonhosted.org/packages/da/67/672b422d9daf07365259958912ba533a0ecab839d4084c487a5fe9a5405f/requests-2.24.0.tar.gz"
sha256 "b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" sha256 "b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"
end end
resource "certifi" do resource "certifi" do
url "https://files.pythonhosted.org/packages/b8/e2/a3a86a67c3fc8249ed305fc7b7d290ebe5e4d46ad45573884761ef4dea7b/certifi-2020.4.5.1.tar.gz" url "https://files.pythonhosted.org/packages/b4/19/53433f37a31543364c8676f30b291d128cdf4cd5b31b755b7890f8e89ac8/certifi-2020.4.5.2.tar.gz"
sha256 "51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" sha256 "5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"
end end
resource "urllib3" do resource "urllib3" do

View File

@ -1,8 +1,8 @@
""" """
HTTPie - a CLI, cURL-like tool for humans. HTTPie: command-line HTTP client for the API era.
""" """
__version__ = '2.2.0' __version__ = '2.3.0'
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__licence__ = 'BSD' __licence__ = 'BSD'

View File

@ -15,12 +15,11 @@ from httpie.cli.argtypes import (
parse_format_options, parse_format_options,
) )
from httpie.cli.constants import ( from httpie.cli.constants import (
DEFAULT_FORMAT_OPTIONS, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
OUTPUT_OPTIONS_DEFAULT, OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED, OUT_RESP_BODY, PRETTY_MAP, OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
PRETTY_STDOUT_TTY_ONLY, SEPARATOR_CREDENTIALS, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_CREDENTIALS,
SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE,
OUTPUT_OPTIONS_DEFAULT_OFFLINE,
) )
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
from httpie.cli.requestitems import RequestItems from httpie.cli.requestitems import RequestItems
@ -75,18 +74,16 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
) -> argparse.Namespace: ) -> argparse.Namespace:
self.env = env self.env = env
self.args, no_options = super().parse_known_args(args, namespace) self.args, no_options = super().parse_known_args(args, namespace)
if self.args.debug: if self.args.debug:
self.args.traceback = True self.args.traceback = True
self.has_stdin_data = ( self.has_stdin_data = (
self.env.stdin self.env.stdin
and not self.args.ignore_stdin and not self.args.ignore_stdin
and not self.env.stdin_isatty and not self.env.stdin_isatty
) )
# Arguments processing and environment setup. # Arguments processing and environment setup.
self._apply_no_options(no_options) self._apply_no_options(no_options)
self._process_request_type()
self._process_download_options() self._process_download_options()
self._setup_standard_streams() self._setup_standard_streams()
self._process_output_options() self._process_output_options()
@ -94,14 +91,35 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self._process_format_options() self._process_format_options()
self._guess_method() self._guess_method()
self._parse_items() self._parse_items()
if self.has_stdin_data: if self.has_stdin_data:
self._body_from_file(self.env.stdin) self._body_from_file(self.env.stdin)
self._process_url()
self._process_auth()
if self.args.compress:
# TODO: allow --compress with --chunked / --multipart
if self.args.chunked:
self.error('cannot combine --compress and --chunked')
if self.args.multipart:
self.error('cannot combine --compress and --multipart')
return self.args
def _process_request_type(self):
request_type = self.args.request_type
self.args.json = request_type is RequestType.JSON
self.args.multipart = request_type is RequestType.MULTIPART
self.args.form = request_type in {
RequestType.FORM,
RequestType.MULTIPART,
}
def _process_url(self):
if not URL_SCHEME_RE.match(self.args.url): if not URL_SCHEME_RE.match(self.args.url):
if os.path.basename(env.program_name) == 'https': if os.path.basename(self.env.program_name) == 'https':
scheme = 'https://' scheme = 'https://'
else: else:
scheme = self.args.default_scheme + "://" scheme = self.args.default_scheme + '://'
# See if we're using curl style shorthand for localhost (:3000/foo) # See if we're using curl style shorthand for localhost (:3000/foo)
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url) shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
@ -114,9 +132,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.url += rest self.args.url += rest
else: else:
self.args.url = scheme + self.args.url self.args.url = scheme + self.args.url
self._process_auth()
return self.args
# noinspection PyShadowingBuiltins # noinspection PyShadowingBuiltins
def _print_message(self, message, file=None): def _print_message(self, message, file=None):
@ -135,6 +150,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed. Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
""" """
self.args.output_file_specified = bool(self.args.output_file) self.args.output_file_specified = bool(self.args.output_file)
if self.args.download: if self.args.download:
# FIXME: Come up with a cleaner solution. # FIXME: Come up with a cleaner solution.
@ -147,6 +163,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
# The response body will be treated separately. # The response body will be treated separately.
self.env.stdout = self.env.stderr self.env.stdout = self.env.stderr
self.env.stdout_isatty = self.env.stderr_isatty self.env.stdout_isatty = self.env.stderr_isatty
elif self.args.output_file: elif self.args.output_file:
# When not `--download`ing, then `--output` simply replaces # When not `--download`ing, then `--output` simply replaces
# `stdout`. The file is opened for appending, which isn't what # `stdout`. The file is opened for appending, which isn't what
@ -163,6 +180,11 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.env.stdout = self.args.output_file self.env.stdout = self.args.output_file
self.env.stdout_isatty = False self.env.stdout_isatty = False
if self.args.quiet:
self.env.stderr = self.env.devnull
if not (self.args.output_file_specified and not self.args.download):
self.env.stdout = self.env.devnull
def _process_auth(self): def _process_auth(self):
# TODO: refactor & simplify this method. # TODO: refactor & simplify this method.
self.args.auth_plugin = None self.args.auth_plugin = None
@ -271,7 +293,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
'data (key=value) cannot be mixed. Pass ' 'data (key=value) cannot be mixed. Pass '
'--ignore-stdin to let key/value take priority. ' '--ignore-stdin to let key/value take priority. '
'See https://httpie.org/doc#scripting for details.') 'See https://httpie.org/doc#scripting for details.')
self.args.data = getattr(fd, 'buffer', fd).read() self.args.data = getattr(fd, 'buffer', fd)
def _guess_method(self): def _guess_method(self):
"""Set `args.method` if not specified to either POST or GET """Set `args.method` if not specified to either POST or GET
@ -332,6 +354,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.data = request_items.data self.args.data = request_items.data
self.args.files = request_items.files self.args.files = request_items.files
self.args.params = request_items.params self.args.params = request_items.params
self.args.multipart_data = request_items.multipart_data
if self.args.files and not self.args.form: if self.args.files and not self.args.form:
# `http url @/path/to/file` # `http url @/path/to/file`

View File

@ -242,9 +242,3 @@ PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
s=','.join(DEFAULT_FORMAT_OPTIONS), s=','.join(DEFAULT_FORMAT_OPTIONS),
defaults=None, defaults=None,
) )
class UnsortedAction(argparse.Action):
def __call__(self, *args, **kwargs):
return 1

View File

@ -1,8 +1,8 @@
"""Parsing and processing of CLI input (args, auth credentials, files, stdin). """Parsing and processing of CLI input (args, auth credentials, files, stdin).
""" """
import enum
import re import re
import ssl
# TODO: Use MultiDict for headers once added to `requests`. # TODO: Use MultiDict for headers once added to `requests`.
@ -11,6 +11,9 @@ import ssl
# ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) # ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
# <https://tools.ietf.org/html/rfc3986#section-3.1> # <https://tools.ietf.org/html/rfc3986#section-3.1>
from enum import Enum
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE) URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
HTTP_POST = 'POST' HTTP_POST = 'POST'
@ -38,6 +41,12 @@ SEPARATOR_GROUP_DATA_ITEMS = frozenset({
SEPARATOR_DATA_EMBED_RAW_JSON_FILE SEPARATOR_DATA_EMBED_RAW_JSON_FILE
}) })
SEPARATORS_GROUP_MULTIPART = frozenset({
SEPARATOR_DATA_STRING,
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_FILE_UPLOAD,
})
# Separators for items whose value is a filename to be embedded # Separators for items whose value is a filename to be embedded
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({ SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
SEPARATOR_DATA_EMBED_FILE_CONTENTS, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
@ -103,3 +112,9 @@ UNSORTED_FORMAT_OPTIONS_STRING = ','.join(
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
class RequestType(enum.Enum):
FORM = enum.auto()
MULTIPART = enum.auto()
JSON = enum.auto()

View File

@ -15,7 +15,8 @@ from httpie.cli.constants import (
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD, 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, SORTED_FORMAT_OPTIONS_STRING, RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING, UNSORTED_FORMAT_OPTIONS_STRING,
) )
from httpie.output.formatters.colors import ( from httpie.output.formatters.colors import (
@ -110,7 +111,7 @@ positional.add_argument(
awesome:=true amount:=42 colors:='["red", "green", "blue"]' awesome:=true amount:=42 colors:='["red", "green", "blue"]'
'@' Form file fields (only with --form, -f): '@' Form file fields (only with --form or --multipart):
cv@~/Documents/CV.pdf cv@~/Documents/CV.pdf
cv@'~/Documents/CV.pdf;type=application/pdf' cv@'~/Documents/CV.pdf;type=application/pdf'
@ -141,7 +142,9 @@ content_type = parser.add_argument_group(
content_type.add_argument( content_type.add_argument(
'--json', '-j', '--json', '-j',
action='store_true', action='store_const',
const=RequestType.JSON,
dest='request_type',
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
@ -151,7 +154,9 @@ content_type.add_argument(
) )
content_type.add_argument( content_type.add_argument(
'--form', '-f', '--form', '-f',
action='store_true', action='store_const',
const=RequestType.FORM,
dest='request_type',
help=''' help='''
Data items from the command line are serialized as form fields. Data items from the command line are serialized as form fields.
@ -161,6 +166,26 @@ content_type.add_argument(
''' '''
) )
content_type.add_argument(
'--multipart',
action='store_const',
const=RequestType.MULTIPART,
dest='request_type',
help='''
Similar to --form, but always sends a multipart/form-data
request (i.e., even without files).
'''
)
content_type.add_argument(
'--boundary',
help='''
Specify a custom boundary string for multipart/form-data requests.
Only has effect only together with --form.
'''
)
####################################################################### #######################################################################
# Content processing. # Content processing.
@ -375,7 +400,7 @@ output_options.add_argument(
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 response body 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),
HTTPie fetches the whole response before it outputs the processed data. HTTPie fetches the whole response before it outputs the processed data.
@ -426,6 +451,17 @@ output_options.add_argument(
''' '''
) )
output_options.add_argument(
'--quiet', '-q',
action='store_true',
default=False,
help='''
Do not print to stdout or stderr.
stdout is still redirected if --output is specified.
Flag doesn't affect behaviour of download beyond not printing to terminal.
'''
)
####################################################################### #######################################################################
# Sessions # Sessions
####################################################################### #######################################################################
@ -627,6 +663,15 @@ network.add_argument(
''' '''
) )
network.add_argument(
'--chunked',
default=False,
action='store_true',
help="""
"""
)
####################################################################### #######################################################################
# SSL # SSL
####################################################################### #######################################################################
@ -643,7 +688,7 @@ ssl.add_argument(
''' '''
) )
ssl.add_argument( ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol? '--ssl',
dest='ssl_version', dest='ssl_version',
choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())), choices=list(sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())),
help=''' help='''

View File

@ -34,20 +34,25 @@ class MultiValueOrderedDict(OrderedDict):
super().__setitem__(key, [self[key]]) super().__setitem__(key, [self[key]])
self[key].append(value) self[key].append(value)
class RequestQueryParamsDict(MultiValueOrderedDict):
pass
class RequestDataDict(MultiValueOrderedDict):
def items(self): def items(self):
for key, values in super(MultiValueOrderedDict, self).items(): for key, values in super().items():
if not isinstance(values, list): if not isinstance(values, list):
values = [values] values = [values]
for value in values: for value in values:
yield key, value yield key, value
class RequestQueryParamsDict(MultiValueOrderedDict):
pass
class RequestDataDict(MultiValueOrderedDict):
pass
class MultipartRequestDataDict(MultiValueOrderedDict):
pass
class RequestFilesDict(RequestDataDict): class RequestFilesDict(RequestDataDict):
pass pass

View File

@ -4,13 +4,15 @@ from typing import Callable, Dict, IO, List, Optional, Tuple, Union
from httpie.cli.argtypes import KeyValueArg 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, SEPARATORS_GROUP_MULTIPART, 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_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY, SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
SEPARATOR_QUERY_PARAM, SEPARATOR_QUERY_PARAM,
) )
from httpie.cli.dicts import ( from httpie.cli.dicts import (
RequestDataDict, RequestFilesDict, RequestHeadersDict, RequestJSONDataDict, MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
RequestHeadersDict, RequestJSONDataDict,
RequestQueryParamsDict, RequestQueryParamsDict,
) )
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
@ -24,6 +26,8 @@ class RequestItems:
self.data = RequestDataDict() if as_form else RequestJSONDataDict() self.data = RequestDataDict() if as_form else RequestJSONDataDict()
self.files = RequestFilesDict() self.files = RequestFilesDict()
self.params = RequestQueryParamsDict() self.params = RequestQueryParamsDict()
# To preserve the order of fields in file upload multipart requests.
self.multipart_data = MultipartRequestDataDict()
@classmethod @classmethod
def from_args( def from_args(
@ -69,7 +73,11 @@ class RequestItems:
for arg in request_item_args: for arg in request_item_args:
processor_func, target_dict = rules[arg.sep] processor_func, target_dict = rules[arg.sep]
target_dict[arg.key] = processor_func(arg) value = processor_func(arg)
target_dict[arg.key] = value
if arg.sep in SEPARATORS_GROUP_MULTIPART:
instance.multipart_data[arg.key] = value
return instance return instance
@ -99,15 +107,13 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE) parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
filename = parts[0] filename = parts[0]
mime_type = parts[1] if len(parts) > 1 else None mime_type = parts[1] if len(parts) > 1 else None
try: try:
with open(os.path.expanduser(filename), 'rb') as f: f = open(os.path.expanduser(filename), 'rb')
contents = f.read()
except IOError as e: except IOError as e:
raise ParseError('"%s": %s' % (arg.orig, e)) raise ParseError('"%s": %s' % (arg.orig, e))
return ( return (
os.path.basename(filename), os.path.basename(filename),
BytesIO(contents), f,
mime_type or get_content_type(filename), mime_type or get_content_type(filename),
) )

View File

@ -2,23 +2,26 @@ import argparse
import http.client import http.client
import json import json
import sys import sys
import zlib
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Iterable, Union from typing import Callable, Iterable, Union
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import urllib3 import urllib3
from httpie import __version__ from httpie import __version__
from httpie.cli.dicts import RequestHeadersDict from httpie.cli.dicts import RequestHeadersDict
from httpie.plugins.registry 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.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
from httpie.uploads import (
compress_request, prepare_request_body,
get_multipart_data_and_content_type,
)
from httpie.utils import get_expired_cookies, repr_dict from httpie.utils import get_expired_cookies, repr_dict
urllib3.disable_warnings() 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'
@ -30,6 +33,7 @@ DEFAULT_UA = f'HTTPie/{__version__}'
def collect_messages( def collect_messages(
args: argparse.Namespace, args: argparse.Namespace,
config_dir: Path, config_dir: Path,
request_body_read_callback: Callable[[bytes], None] = None,
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]: ) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
httpie_session = None httpie_session = None
httpie_session_headers = None httpie_session_headers = None
@ -45,6 +49,7 @@ def collect_messages(
request_kwargs = make_request_kwargs( request_kwargs = make_request_kwargs(
args=args, args=args,
base_headers=httpie_session_headers, base_headers=httpie_session_headers,
request_body_read_callback=request_body_read_callback
) )
send_kwargs = make_send_kwargs(args) send_kwargs = make_send_kwargs(args)
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args) send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
@ -79,7 +84,10 @@ def collect_messages(
prepped_url=prepared_request.url, prepped_url=prepared_request.url,
) )
if args.compress and prepared_request.body: if args.compress and prepared_request.body:
compress_body(prepared_request, always=args.compress > 1) compress_request(
request=prepared_request,
always=args.compress > 1,
)
response_count = 0 response_count = 0
expired_cookies = [] expired_cookies = []
while prepared_request: while prepared_request:
@ -136,22 +144,6 @@ def max_headers(limit):
http.client._MAXHEADERS = orig http.client._MAXHEADERS = orig
def compress_body(request: requests.PreparedRequest, always: bool):
deflater = zlib.compressobj()
body_bytes = (
request.body
if isinstance(request.body, bytes)
else request.body.encode()
)
deflated_data = deflater.compress(body_bytes)
deflated_data += deflater.flush()
is_economical = len(deflated_data) < len(body_bytes)
if is_economical or always:
request.body = deflated_data
request.headers['Content-Encoding'] = 'deflate'
request.headers['Content-Length'] = str(len(deflated_data))
def build_requests_session( def build_requests_session(
verify: bool, verify: bool,
ssl_version: str = None, ssl_version: str = None,
@ -250,12 +242,14 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
def make_request_kwargs( def make_request_kwargs(
args: argparse.Namespace, args: argparse.Namespace,
base_headers: RequestHeadersDict = None base_headers: RequestHeadersDict = None,
request_body_read_callback=lambda chunk: chunk
) -> dict: ) -> dict:
""" """
Translate our `args` into `requests.Request` keyword arguments. Translate our `args` into `requests.Request` keyword arguments.
""" """
files = args.files
# Serialize JSON data, if needed. # Serialize JSON data, if needed.
data = args.data data = args.data
auto_json = data and not args.form auto_json = data and not args.form
@ -272,16 +266,32 @@ def make_request_kwargs(
if base_headers: if base_headers:
headers.update(base_headers) headers.update(base_headers)
headers.update(args.headers) headers.update(args.headers)
if args.offline and args.chunked and 'Transfer-Encoding' not in headers:
# When online, we let requests set the header instead to be able more
# easily verify chunking is taking place.
headers['Transfer-Encoding'] = 'chunked'
headers = finalize_headers(headers) headers = finalize_headers(headers)
if (args.form and files) or args.multipart:
data, headers['Content-Type'] = get_multipart_data_and_content_type(
data=args.multipart_data,
boundary=args.boundary,
content_type=args.headers.get('Content-Type'),
)
kwargs = { kwargs = {
'method': args.method.lower(), 'method': args.method.lower(),
'url': args.url, 'url': args.url,
'headers': headers, 'headers': headers,
'data': data, 'data': prepare_request_body(
body=data,
body_read_callback=request_body_read_callback,
chunked=args.chunked,
offline=args.offline,
content_length_header_value=headers.get('Content-Length'),
),
'auth': args.auth, 'auth': args.auth,
'params': args.params, 'params': args.params.items(),
'files': args.files,
} }
return kwargs return kwargs

View File

@ -108,16 +108,14 @@ class BaseConfigDict(dict):
self.ensure_directory() self.ensure_directory()
try: json_string = json.dumps(
with self.path.open('w') as f:
json.dump(
obj=self, obj=self,
fp=f,
indent=4, indent=4,
sort_keys=True, sort_keys=True,
ensure_ascii=True, ensure_ascii=True,
) )
f.write('\n') try:
self.path.write_text(json_string + '\n')
except IOError: except IOError:
if not fail_silently: if not fail_silently:
raise raise

View File

@ -1,4 +1,5 @@
import sys import sys
import os
from pathlib import Path from pathlib import Path
from typing import IO, Optional from typing import IO, Optional
@ -56,7 +57,7 @@ class Environment:
) )
del colorama del colorama
def __init__(self, **kwargs): def __init__(self, devnull=None, **kwargs):
""" """
Use keyword arguments to overwrite Use keyword arguments to overwrite
any of the class attributes for this instance. any of the class attributes for this instance.
@ -65,6 +66,10 @@ class Environment:
assert all(hasattr(type(self), attr) for attr in kwargs.keys()) assert all(hasattr(type(self), attr) for attr in kwargs.keys())
self.__dict__.update(**kwargs) self.__dict__.update(**kwargs)
# The original STDERR unaffected by --quieting.
self._orig_stderr = self.stderr
self._devnull = devnull
# Keyword arguments > stream.encoding > default utf8 # Keyword arguments > stream.encoding > default utf8
if self.stdin and self.stdin_encoding is None: if self.stdin and self.stdin_encoding is None:
self.stdin_encoding = getattr( self.stdin_encoding = getattr(
@ -108,6 +113,16 @@ class Environment:
self.log_error(e, level='warning') self.log_error(e, level='warning')
return config return config
@property
def devnull(self) -> IO:
if self._devnull is None:
self._devnull = open(os.devnull, 'w+')
return self._devnull
@devnull.setter
def devnull(self, value):
self._devnull = value
def log_error(self, msg, level='error'): def log_error(self, msg, level='error'):
assert level in ['error', 'warning'] assert level in ['error', 'warning']
self.stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')

View File

@ -2,17 +2,24 @@ import argparse
import os import os
import platform import platform
import sys import sys
from typing import List, Union from typing import List, Optional, Tuple, Union
import requests import requests
from pygments import __version__ as pygments_version from pygments import __version__ as pygments_version
from requests import __version__ as requests_version from requests import __version__ as requests_version
from httpie import __version__ as httpie_version from httpie import __version__ as httpie_version
from httpie.cli.constants import (
OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY,
OUT_RESP_HEAD,
)
from httpie.client import collect_messages 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.registry 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
@ -111,6 +118,22 @@ def main(
return exit_status return exit_status
def get_output_options(
args: argparse.Namespace,
message: Union[requests.PreparedRequest, requests.Response]
) -> Tuple[bool, bool]:
return {
requests.PreparedRequest: (
OUT_REQ_HEAD in args.output_options,
OUT_REQ_BODY in args.output_options,
),
requests.Response: (
OUT_RESP_HEAD in args.output_options,
OUT_RESP_BODY in args.output_options,
),
}[type(message)]
def program( def program(
args: argparse.Namespace, args: argparse.Namespace,
env: Environment, env: Environment,
@ -132,18 +155,57 @@ def program(
) )
downloader.pre_request(args.headers) downloader.pre_request(args.headers)
initial_request = None needs_separator = False
final_response = None
for message in collect_messages(args, env.config.directory): def maybe_separate():
nonlocal needs_separator
if env.stdout.isatty() and needs_separator:
needs_separator = False
getattr(env.stdout, 'buffer', env.stdout).write(b'\n\n')
initial_request: Optional[requests.PreparedRequest] = None
final_response: Optional[requests.Response] = None
def request_body_read_callback(chunk: bytes):
should_pipe_to_stdout = (
# Request body output desired
OUT_REQ_BODY in args.output_options
# & not `.read()` already pre-request (e.g., for compression)
and initial_request
# & non-EOF chunk
and chunk
)
if should_pipe_to_stdout:
msg = requests.PreparedRequest()
msg.is_body_upload_chunk = True
msg.body = chunk
msg.headers = initial_request.headers
write_message( write_message(
requests_message=message, requests_message=msg,
env=env, env=env,
args=args, args=args,
with_body=True,
with_headers=False
) )
if isinstance(message, requests.PreparedRequest):
messages = collect_messages(
args=args,
config_dir=env.config.directory,
request_body_read_callback=request_body_read_callback
)
for message in messages:
maybe_separate()
is_request = isinstance(message, requests.PreparedRequest)
with_headers, with_body = get_output_options(
args=args, message=message)
if is_request:
if not initial_request: if not initial_request:
initial_request = message initial_request = message
is_streamed_upload = not isinstance(
message.body, (str, bytes))
if with_body:
with_body = not is_streamed_upload
needs_separator = is_streamed_upload
else: else:
final_response = message final_response = message
if args.check_status or downloader: if args.check_status or downloader:
@ -157,6 +219,15 @@ def program(
f'HTTP {message.raw.status} {message.raw.reason}', f'HTTP {message.raw.status} {message.raw.reason}',
level='warning' level='warning'
) )
write_message(
requests_message=message,
env=env,
args=args,
with_headers=with_headers,
with_body=with_body,
)
maybe_separate()
if downloader and exit_status == ExitStatus.SUCCESS: if downloader and exit_status == ExitStatus.SUCCESS:
# Last response body download. # Last response body download.

View File

@ -86,7 +86,7 @@ class ColorFormatter(FormatterPlugin):
lexer=lexer, lexer=lexer,
formatter=self.formatter, formatter=self.formatter,
) )
return body.strip() return body
def get_lexer_for_body( def get_lexer_for_body(
self, mime: str, self, mime: str,

View File

@ -14,10 +14,13 @@ BINARY_SUPPRESSED_NOTICE = (
) )
class BinarySuppressedError(Exception): class DataSuppressedError(Exception):
message = None
class BinarySuppressedError(DataSuppressedError):
"""An error indicating that the body is binary and won't be written, """An error indicating that the body is binary and won't be written,
e.g., for terminal output).""" e.g., for terminal output)."""
message = BINARY_SUPPRESSED_NOTICE message = BINARY_SUPPRESSED_NOTICE
@ -63,7 +66,7 @@ class BaseStream:
yield chunk yield chunk
if self.on_body_chunk_downloaded: if self.on_body_chunk_downloaded:
self.on_body_chunk_downloaded(chunk) self.on_body_chunk_downloaded(chunk)
except BinarySuppressedError as e: except DataSuppressedError as e:
if self.with_headers: if self.with_headers:
yield b'\n' yield b'\n'
yield e.message yield e.message

View File

@ -1,6 +1,6 @@
import argparse import argparse
import errno import errno
from typing import Union, IO, TextIO, Tuple, Type from typing import IO, TextIO, Tuple, Type, Union
import requests import requests
@ -8,12 +8,7 @@ from httpie.context import Environment
from httpie.models import HTTPRequest, HTTPResponse from httpie.models import HTTPRequest, HTTPResponse
from httpie.output.processing import Conversion, Formatting from httpie.output.processing import Conversion, Formatting
from httpie.output.streams import ( from httpie.output.streams import (
RawStream, PrettyStream, BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
BufferedPrettyStream, EncodedStream,
BaseStream,
)
from httpie.cli.constants import (
OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD,
) )
@ -21,26 +16,18 @@ def write_message(
requests_message: Union[requests.PreparedRequest, requests.Response], requests_message: Union[requests.PreparedRequest, requests.Response],
env: Environment, env: Environment,
args: argparse.Namespace, args: argparse.Namespace,
with_headers=False,
with_body=False,
): ):
output_options_by_message_type = { if not (with_body or with_headers):
requests.PreparedRequest: {
'with_headers': OUT_REQ_HEAD in args.output_options,
'with_body': OUT_REQ_BODY in args.output_options,
},
requests.Response: {
'with_headers': OUT_RESP_HEAD in args.output_options,
'with_body': OUT_RESP_BODY in args.output_options,
},
}
output_options = output_options_by_message_type[type(requests_message)]
if not any(output_options.values()):
return return
write_stream_kwargs = { write_stream_kwargs = {
'stream': build_output_stream_for_message( 'stream': build_output_stream_for_message(
args=args, args=args,
env=env, env=env,
requests_message=requests_message, requests_message=requests_message,
**output_options, with_body=with_body,
with_headers=with_headers,
), ),
# NOTE: `env.stdout` will in fact be `stderr` with `--download` # NOTE: `env.stdout` will in fact be `stderr` with `--download`
'outfile': env.stdout, 'outfile': env.stdout,
@ -120,7 +107,8 @@ def build_output_stream_for_message(
with_body=with_body, with_body=with_body,
**stream_kwargs, **stream_kwargs,
) )
if env.stdout_isatty and with_body: if (env.stdout_isatty and with_body
and not getattr(requests_message, 'is_body_upload_chunk', False)):
# Ensure a blank line after the response body. # Ensure a blank line after the response body.
# For terminal output only. # For terminal output only.
yield b'\n\n' yield b'\n\n'

View File

@ -4,6 +4,8 @@ Persistent, JSON-serialized sessions.
""" """
import os import os
import re import re
from http.cookies import SimpleCookie
from pathlib import Path from pathlib import Path
from typing import Iterable, Optional, Union from typing import Iterable, Optional, Union
from urllib.parse import urlsplit from urllib.parse import urlsplit
@ -76,7 +78,13 @@ class Session(BaseConfigDict):
continue # Ignore explicitly unset headers continue # Ignore explicitly unset headers
value = value.decode('utf8') value = value.decode('utf8')
if name == 'User-Agent' and value.startswith('HTTPie/'): if name.lower() == 'user-agent' and value.startswith('HTTPie/'):
continue
if name.lower() == 'cookie':
for cookie_name, morsel in SimpleCookie(value).items():
self['cookies'][cookie_name] = {'value': morsel.value}
del request_headers[name]
continue continue
for prefix in SESSION_IGNORED_HEADER_PREFIXES: for prefix in SESSION_IGNORED_HEADER_PREFIXES:

View File

@ -52,7 +52,7 @@ class HTTPieHTTPSAdapter(HTTPAdapter):
verify: bool, verify: bool,
ssl_version: str = None, ssl_version: str = None,
ciphers: str = None, ciphers: str = None,
) -> ssl.SSLContext: ) -> 'ssl.SSLContext':
return create_urllib3_context( return create_urllib3_context(
ciphers=ciphers, ciphers=ciphers,
ssl_version=resolve_ssl_version(ssl_version), ssl_version=resolve_ssl_version(ssl_version),

138
httpie/uploads.py Normal file
View File

@ -0,0 +1,138 @@
import zlib
from typing import Callable, IO, Iterable, Tuple, Union
from urllib.parse import urlencode
import requests
from requests.utils import super_len
from requests_toolbelt import MultipartEncoder
from httpie.cli.dicts import MultipartRequestDataDict, RequestDataDict
class ChunkedUploadStream:
def __init__(self, stream: Iterable, callback: Callable):
self.callback = callback
self.stream = stream
def __iter__(self) -> Iterable[Union[str, bytes]]:
for chunk in self.stream:
self.callback(chunk)
yield chunk
class ChunkedMultipartUploadStream:
chunk_size = 100 * 1024
def __init__(self, encoder: MultipartEncoder):
self.encoder = encoder
def __iter__(self) -> Iterable[Union[str, bytes]]:
while True:
chunk = self.encoder.read(self.chunk_size)
if not chunk:
break
yield chunk
def prepare_request_body(
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
body_read_callback: Callable[[bytes], bytes],
content_length_header_value: int = None,
chunked=False,
offline=False,
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
is_file_like = hasattr(body, 'read')
if isinstance(body, RequestDataDict):
body = urlencode(body, doseq=True)
if offline:
if is_file_like:
return body.read()
return body
if not is_file_like:
if chunked:
body = ChunkedUploadStream(
# Pass the entire body as one chunk.
stream=(chunk.encode() for chunk in [body]),
callback=body_read_callback,
)
else:
# File-like object.
if not super_len(body):
# Zero-length -> assume stdin.
if content_length_header_value is None and not chunked:
#
# Read the whole stdin to determine `Content-Length`.
#
# TODO: Instead of opt-in --chunked, consider making
# `Transfer-Encoding: chunked` for STDIN opt-out via
# something like --no-chunked.
# This would be backwards-incompatible so wait until v3.0.0.
#
body = body.read()
else:
orig_read = body.read
def new_read(*args):
chunk = orig_read(*args)
body_read_callback(chunk)
return chunk
body.read = new_read
if chunked:
if isinstance(body, MultipartEncoder):
body = ChunkedMultipartUploadStream(
encoder=body,
)
else:
body = ChunkedUploadStream(
stream=body,
callback=body_read_callback,
)
return body
def get_multipart_data_and_content_type(
data: MultipartRequestDataDict,
boundary: str = None,
content_type: str = None,
) -> Tuple[MultipartEncoder, str]:
encoder = MultipartEncoder(
fields=data.items(),
boundary=boundary,
)
if content_type:
content_type = content_type.strip()
if 'boundary=' not in content_type:
content_type = f'{content_type}; boundary={encoder.boundary_value}'
else:
content_type = encoder.content_type
data = encoder
return data, content_type
def compress_request(
request: requests.PreparedRequest,
always: bool,
):
deflater = zlib.compressobj()
if isinstance(request.body, str):
body_bytes = request.body.encode()
elif hasattr(request.body, 'read'):
body_bytes = request.body.read()
else:
body_bytes = request.body
deflated_data = deflater.compress(body_bytes)
deflated_data += deflater.flush()
is_economical = len(deflated_data) < len(body_bytes)
if is_economical or always:
request.body = deflated_data
request.headers['Content-Encoding'] = 'deflate'
request.headers['Content-Length'] = str(len(deflated_data))

View File

@ -6,7 +6,7 @@ import time
from collections import OrderedDict from collections import OrderedDict
from http.cookiejar import parse_ns_headers from http.cookiejar import parse_ns_headers
from pprint import pformat from pprint import pformat
from typing import List, Tuple from typing import List, Optional, Tuple
import requests.auth import requests.auth
@ -93,7 +93,12 @@ def get_expired_cookies(
headers: List[Tuple[str, str]], headers: List[Tuple[str, str]],
now: float = None now: float = None
) -> List[dict]: ) -> List[dict]:
now = now or time.time() now = now or time.time()
def is_expired(expires: Optional[float]) -> bool:
return expires is not None and expires <= now
attr_sets: List[Tuple[str, str]] = parse_ns_headers( attr_sets: List[Tuple[str, str]] = parse_ns_headers(
value for name, value in headers value for name, value in headers
if name.lower() == 'set-cookie' if name.lower() == 'set-cookie'
@ -103,11 +108,12 @@ def get_expired_cookies(
dict(attrs[1:], name=attrs[0][0]) dict(attrs[1:], name=attrs[0][0])
for attrs in attr_sets for attrs in attr_sets
] ]
return [ return [
{ {
'name': cookie['name'], 'name': cookie['name'],
'path': cookie.get('path', '/') 'path': cookie.get('path', '/')
} }
for cookie in cookies for cookie in cookies
if cookie.get('expires', float('Inf')) <= now if is_expired(expires=cookie.get('expires'))
] ]

View File

@ -1,4 +1,3 @@
tox
mock mock
pytest pytest
pytest-cov pytest-cov

View File

@ -10,7 +10,7 @@ addopts = --tb=native
[pycodestyle] [pycodestyle]
# <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration> # <http://pycodestyle.pycqa.org/en/latest/intro.html#configuration>
exclude = .git,.idea,__pycache__,build,dist,.tox,.pytest_cache,*.egg-info exclude = .git,.idea,__pycache__,build,dist,.pytest_cache,*.egg-info
# <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes> # <http://pycodestyle.pycqa.org/en/latest/intro.html#error-codes>
# E241 - multiple spaces after , # E241 - multiple spaces after ,

View File

@ -18,8 +18,10 @@ class PyTest(TestCommand):
def finalize_options(self): def finalize_options(self):
TestCommand.finalize_options(self) TestCommand.finalize_options(self)
self.test_args = [ self.test_args = [
'--doctest-modules', '--verbose', '--doctest-modules',
'./httpie', './tests' '--verbose',
'./httpie',
'./tests',
] ]
self.test_suite = True self.test_suite = True
@ -36,8 +38,9 @@ tests_require = [
install_requires = [ install_requires = [
'requests>=2.22.0', 'requests[socks]>=2.22.0',
'Pygments>=2.5.2', 'Pygments>=2.5.2',
'requests-toolbelt>=0.9.1',
] ]
install_requires_win_only = [ install_requires_win_only = [
'colorama>=0.2.4', 'colorama>=0.2.4',
@ -71,7 +74,7 @@ setup(
description=httpie.__doc__.strip(), description=httpie.__doc__.strip(),
long_description=long_description(), long_description=long_description(),
url='https://httpie.org/', url='https://httpie.org/',
download_url=f'https://github.com/jakubroztocil/httpie/archive/{httpie.__version__}.tar.gz', download_url=f'https://github.com/httpie/httpie/archive/{httpie.__version__}.tar.gz',
author=httpie.__author__, author=httpie.__author__,
author_email='jakub@roztocil.co', author_email='jakub@roztocil.co',
license=httpie.__licence__, license=httpie.__licence__,
@ -103,10 +106,10 @@ setup(
'Topic :: Utilities' 'Topic :: Utilities'
], ],
project_urls={ project_urls={
'GitHub': 'https://github.com/httpie/httpie',
'Twitter': 'https://twitter.com/httpie',
'Documentation': 'https://httpie.org/docs', 'Documentation': 'https://httpie.org/docs',
'Source': 'https://github.com/jakubroztocil/httpie',
'Online Demo': 'https://httpie.org/run', 'Online Demo': 'https://httpie.org/run',
'Donate': 'https://httpie.org/donate', 'Donate': 'https://httpie.org/donate',
'Twitter': 'https://twitter.com/clihttp',
}, },
) )

View File

@ -1,29 +1,31 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -1,51 +1,51 @@
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

View File

@ -1,87 +1,82 @@
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
subject=/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
issuer=/C=US/ST=CA/L=SF/O=HTTPie/CN=HTTPie
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE----- -----END CERTIFICATE-----
Bag Attributes
localKeyID: 93 0C 3E A7 82 62 36 37 5E 73 9B 05 C4 98 DF DC 04 5C B4 C9
Key Attributes: <No Attributes>
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

View File

@ -1,6 +1,5 @@
"""Test data""" """Test data"""
from os import path from pathlib import Path
import codecs
def patharg(path): def patharg(path):
@ -9,32 +8,24 @@ def patharg(path):
even in Windows paths. even in Windows paths.
""" """
return path.replace('\\', '\\\\\\') return str(path).replace('\\', '\\\\\\')
FIXTURES_ROOT = path.join(path.abspath(path.dirname(__file__))) FIXTURES_ROOT = Path(__file__).parent
FILE_PATH = path.join(FIXTURES_ROOT, 'test.txt') FILE_PATH = FIXTURES_ROOT / 'test.txt'
JSON_FILE_PATH = path.join(FIXTURES_ROOT, 'test.json') JSON_FILE_PATH = FIXTURES_ROOT / 'test.json'
BIN_FILE_PATH = path.join(FIXTURES_ROOT, 'test.bin') BIN_FILE_PATH = FIXTURES_ROOT / 'test.bin'
FILE_PATH_ARG = patharg(FILE_PATH) FILE_PATH_ARG = patharg(FILE_PATH)
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH) BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH) JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
with codecs.open(FILE_PATH, encoding='utf8') as f:
# Strip because we don't want new lines in the data so that we can # Strip because we don't want new lines in the data so that we can
# easily count occurrences also when embedded in JSON (where the new # easily count occurrences also when embedded in JSON (where the new
# line would be escaped). # line would be escaped).
FILE_CONTENT = f.read().strip() FILE_CONTENT = FILE_PATH.read_text('utf8').strip()
with codecs.open(JSON_FILE_PATH, encoding='utf8') as f: JSON_FILE_CONTENT = JSON_FILE_PATH.read_text('utf8')
JSON_FILE_CONTENT = f.read() BIN_FILE_CONTENT = BIN_FILE_PATH.read_bytes()
with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read()
UNICODE = FILE_CONTENT UNICODE = FILE_CONTENT

View File

@ -15,7 +15,7 @@ from httpie.cli import constants
from httpie.cli.definition import parser from httpie.cli.definition import parser
from httpie.cli.argtypes import KeyValueArg, KeyValueArgType from httpie.cli.argtypes import KeyValueArg, KeyValueArgType
from httpie.cli.requestitems import RequestItems from httpie.cli.requestitems import RequestItems
from utils import HTTP_OK, MockEnvironment, http from utils import HTTP_OK, MockEnvironment, StdinBytesIO, http
class TestItemParsing: class TestItemParsing:
@ -312,10 +312,11 @@ class TestNoOptions:
class TestStdin: class TestStdin:
def test_ignore_stdin(self, httpbin): def test_ignore_stdin(self, httpbin):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', stdin_isatty=False,
env=env) )
r = http('--ignore-stdin', '--verbose', httpbin.url + '/get', env=env)
assert HTTP_OK in r assert HTTP_OK in r
assert 'GET /get HTTP' in r, "Don't default to POST." assert 'GET /get HTTP' in r, "Don't default to POST."
assert FILE_CONTENT not in r, "Don't send stdin data." assert FILE_CONTENT not in r, "Don't send stdin data."

View File

@ -12,7 +12,8 @@ import base64
import zlib import zlib
from fixtures import FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH, FILE_CONTENT
from utils import http, HTTP_OK, MockEnvironment from httpie.status import ExitStatus
from utils import StdinBytesIO, http, HTTP_OK, MockEnvironment
def assert_decompressed_equal(base64_compressed_data, expected_str): def assert_decompressed_equal(base64_compressed_data, expected_str):
@ -27,6 +28,20 @@ def assert_decompressed_equal(base64_compressed_data, expected_str):
assert actual_str == expected_str assert actual_str == expected_str
def test_cannot_combine_compress_with_chunked(httpbin):
r = http('--compress', '--chunked', httpbin.url + '/get',
tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.ERROR
assert 'cannot combine --compress and --chunked' in r.stderr
def test_cannot_combine_compress_with_multipart(httpbin):
r = http('--compress', '--multipart', httpbin.url + '/get',
tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.ERROR
assert 'cannot combine --compress and --multipart' in r.stderr
def test_compress_skip_negative_ratio(httpbin_both): def test_compress_skip_negative_ratio(httpbin_both):
r = http( r = http(
'--compress', '--compress',
@ -78,8 +93,10 @@ def test_compress_form(httpbin_both):
def test_compress_stdin(httpbin_both): def test_compress_stdin(httpbin_both):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
r = http( r = http(
'--compress', '--compress',
'--compress', '--compress',
@ -100,7 +117,7 @@ def test_compress_file(httpbin_both):
'--compress', '--compress',
'PUT', 'PUT',
httpbin_both + '/put', httpbin_both + '/put',
'file@' + FILE_PATH, f'file@{FILE_PATH}',
) )
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['headers']['Content-Encoding'] == 'deflate' assert r.json['headers']['Content-Encoding'] == 'deflate'

View File

@ -2,6 +2,8 @@
Tests for the provided defaults regarding HTTP method, and --json vs. --form. Tests for the provided defaults regarding HTTP method, and --json vs. --form.
""" """
from io import BytesIO
from httpie.client import JSON_ACCEPT from httpie.client import JSON_ACCEPT
from utils import MockEnvironment, http, HTTP_OK from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH from fixtures import FILE_PATH
@ -44,8 +46,10 @@ class TestImplicitHTTPMethod:
assert r.json['form'] == {'foo': 'bar'} assert r.json['form'] == {'foo': 'bar'}
def test_implicit_POST_stdin(self, httpbin): def test_implicit_POST_stdin(self, httpbin):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin_isatty=False, stdin=f) stdin_isatty=False,
stdin=BytesIO(FILE_PATH.read_bytes())
)
r = http('--form', httpbin.url + '/post', env=env) r = http('--form', httpbin.url + '/post', env=env)
assert HTTP_OK in r assert HTTP_OK in r

View File

@ -40,13 +40,17 @@ assert filenames
# HACK: hardcoded paths, venv should be irrelevant, etc. # HACK: hardcoded paths, venv should be irrelevant, etc.
# TODO: replaces the process with Python code # TODO: simplify by using the Python API instead of a subprocess
# then we wontt need the paths.
VENV_BIN = Path(__file__).parent.parent / 'venv/bin' VENV_BIN = Path(__file__).parent.parent / 'venv/bin'
VENV_PYTHON = VENV_BIN / 'python' VENV_PYTHON = VENV_BIN / 'python'
VENV_RST2PSEUDOXML = VENV_BIN / 'rst2pseudoxml.py' VENV_RST2PSEUDOXML = VENV_BIN / 'rst2pseudoxml.py'
@pytest.mark.skipif(not os.path.exists(VENV_RST2PSEUDOXML), reason='docutils not installed') @pytest.mark.skipif(
not VENV_RST2PSEUDOXML.exists(),
reason='docutils not installed',
)
@pytest.mark.parametrize('filename', filenames) @pytest.mark.parametrize('filename', filenames)
def test_rst_file_syntax(filename): def test_rst_file_syntax(filename):
p = subprocess.Popen( p = subprocess.Popen(

View File

@ -5,6 +5,7 @@ from urllib.request import urlopen
import pytest import pytest
import mock import mock
import requests
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from httpie.downloads import ( from httpie.downloads import (
@ -182,7 +183,8 @@ class TestDownloads:
# Redirect from `/redirect/1` to `/get`. # Redirect from `/redirect/1` to `/get`.
expected_filename = '1.json' expected_filename = '1.json'
orig_cwd = os.getcwd() orig_cwd = os.getcwd()
os.chdir(tempfile.mkdtemp(prefix='httpie_download_test_')) with tempfile.TemporaryDirectory() as tmp_dirname:
os.chdir(tmp_dirname)
try: try:
assert os.listdir('.') == [] assert os.listdir('.') == []
http('--download', httpbin.url + '/redirect/1') http('--download', httpbin.url + '/redirect/1')

View File

@ -4,14 +4,13 @@ from unittest import mock
import pytest import pytest
import httpie
import httpie.__main__ import httpie.__main__
from fixtures import FILE_CONTENT, FILE_PATH
from httpie.cli.exceptions import ParseError
from httpie.context import Environment from httpie.context import Environment
from httpie.status import ExitStatus from httpie.status import ExitStatus
from httpie.cli.exceptions import ParseError from utils import HTTP_OK, MockEnvironment, StdinBytesIO, http
from utils import MockEnvironment, http, HTTP_OK
from fixtures import FILE_PATH, FILE_CONTENT
import httpie
def test_main_entry_point(): def test_main_entry_point():
@ -46,7 +45,6 @@ def test_help():
def test_version(): def test_version():
r = http('--version', tolerate_error_exit_status=True) r = http('--version', tolerate_error_exit_status=True)
assert r.exit_status == ExitStatus.SUCCESS assert r.exit_status == ExitStatus.SUCCESS
# FIXME: py3 has version in stdout, py2 in stderr
assert httpie.__version__ == r.strip() assert httpie.__version__ == r.strip()
@ -100,19 +98,23 @@ def test_POST_form(httpbin_both):
def test_POST_form_multiple_values(httpbin_both): def test_POST_form_multiple_values(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar', 'foo=baz') r = http('--form', 'POST', httpbin_both + '/post', 'foo=bar', 'foo=baz')
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['form'] == {'foo': ['bar', 'baz']} assert r.json['form'] == {
'foo': ['bar', 'baz']
}
def test_POST_stdin(httpbin_both): def test_POST_stdin(httpbin_both):
with open(FILE_PATH) as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
r = http('--form', 'POST', httpbin_both + '/post', env=env) r = http('--form', 'POST', httpbin_both + '/post', env=env)
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
def test_POST_file(httpbin_both): def test_POST_file(httpbin_both):
r = http('--form', 'POST', httpbin_both + '/post', 'file@' + FILE_PATH) r = http('--form', 'POST', httpbin_both + '/post', f'file@{FILE_PATH}')
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r assert FILE_CONTENT in r
@ -127,10 +129,10 @@ def test_form_POST_file_redirected_stdin(httpbin):
'--form', '--form',
'POST', 'POST',
httpbin + '/post', httpbin + '/post',
'file@' + FILE_PATH, f'file@{FILE_PATH}',
tolerate_error_exit_status=True, tolerate_error_exit_status=True,
env=MockEnvironment( env=MockEnvironment(
stdin=f, stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False, stdin_isatty=False,
), ),
) )
@ -181,21 +183,3 @@ def test_json_input_preserve_order(httpbin_both):
assert HTTP_OK in r assert HTTP_OK in r
assert r.json['data'] == \ assert r.json['data'] == \
'{"order": {"map": {"1": "first", "2": "second"}}}' '{"order": {"map": {"1": "first", "2": "second"}}}'
def test_offline():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r
def test_offline_download():
"""Absence of response should be handled gracefully with --download"""
r = http(
'--offline',
'--download',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r

75
tests/test_offline.py Normal file
View File

@ -0,0 +1,75 @@
from fixtures import FILE_CONTENT, FILE_PATH_ARG
from utils import http
def test_offline():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r
def test_offline_form():
r = http(
'--offline',
'--form',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert 'foo=bar' in r
def test_offline_json():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert r.json == {'foo': 'bar'}
def test_offline_multipart():
r = http(
'--offline',
'--multipart',
'https://this-should.never-resolve/foo',
'foo=bar'
)
assert 'POST /foo' in r
assert 'name="foo"' in r
def test_offline_from_file():
r = http(
'--offline',
'https://this-should.never-resolve/foo',
f'@{FILE_PATH_ARG}'
)
assert 'POST /foo' in r
assert FILE_CONTENT in r
def test_offline_chunked():
r = http(
'--offline',
'--chunked',
'--form',
'https://this-should.never-resolve/foo',
'hello=world'
)
assert 'POST /foo' in r
assert 'Transfer-Encoding: chunked' in r, r
assert 'hello=world' in r
def test_offline_download():
"""Absence of response should be handled gracefully with --download"""
r = http(
'--offline',
'--download',
'https://this-should.never-resolve/foo',
)
assert 'GET /foo' in r

View File

@ -1,10 +1,16 @@
import argparse import argparse
from pathlib import Path
import mock
import json import json
import os import os
import tempfile
import io
from tempfile import gettempdir from tempfile import gettempdir
from urllib.request import urlopen from urllib.request import urlopen
import pytest import pytest
import requests
from httpie.cli.argtypes import ( from httpie.cli.argtypes import (
PARSED_DEFAULT_FORMAT_OPTIONS, PARSED_DEFAULT_FORMAT_OPTIONS,
@ -32,6 +38,87 @@ def test_output_option(httpbin, stdout_isatty):
assert actual_body == expected_body assert actual_body == expected_body
class TestQuietFlag:
@pytest.mark.parametrize('argument_name', ['--quiet', '-q'])
def test_quiet(self, httpbin, argument_name):
env = MockEnvironment(
stdin_isatty=True,
stdout_isatty=True,
devnull=io.BytesIO()
)
r = http(argument_name, 'GET', httpbin.url + '/get', env=env)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert HTTP_OK in r.devnull
assert r == ''
assert r.stderr == ''
@mock.patch('httpie.cli.argtypes.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_quiet_with_password_prompt(self, httpbin):
"""
Tests whether httpie still prompts for a password when request
requires authentication and only username is provided
"""
env = MockEnvironment(
stdin_isatty=True,
stdout_isatty=True,
devnull=io.BytesIO()
)
r = http(
'--quiet', '--auth', 'user', 'GET',
httpbin.url + '/basic-auth/user/password',
env=env
)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert HTTP_OK in r.devnull
assert r == ''
assert r.stderr == ''
@pytest.mark.parametrize('argument_name', ['-h', '-b', '-v', '-p=hH'])
def test_quiet_with_explicit_output_options(self, httpbin, argument_name):
env = MockEnvironment(stdin_isatty=True, stdout_isatty=True)
r = http('--quiet', argument_name, httpbin.url + '/get', env=env)
assert env.stdout is env.devnull
assert env.stderr is env.devnull
assert r == ''
assert r.stderr == ''
@pytest.mark.parametrize('with_download', [True, False])
def test_quiet_with_output_redirection(self, httpbin, with_download):
url = httpbin + '/robots.txt'
output_path = Path('output.txt')
env = MockEnvironment()
orig_cwd = os.getcwd()
output = requests.get(url).text
extra_args = ['--download'] if with_download else []
with tempfile.TemporaryDirectory() as tmp_dirname:
os.chdir(tmp_dirname)
try:
assert os.listdir('.') == []
r = http(
'--quiet',
'--output', str(output_path),
*extra_args,
url,
env=env
)
assert os.listdir('.') == [str(output_path)]
assert r == ''
assert r.stderr == ''
assert env.stderr is env.devnull
if with_download:
assert env.stdout is env.devnull
else:
assert env.stdout is not env.devnull # --output swaps stdout.
assert output_path.read_text() == output
finally:
os.chdir(orig_cwd)
class TestVerboseFlag: class TestVerboseFlag:
def test_verbose(self, httpbin): def test_verbose(self, httpbin):
r = http('--verbose', r = http('--verbose',

View File

@ -3,15 +3,19 @@ import json
import os import os
import shutil import shutil
from datetime import datetime from datetime import datetime
from mock import mock
from tempfile import gettempdir from tempfile import gettempdir
import pytest import pytest
from fixtures import UNICODE from fixtures import UNICODE
from httpie.plugins import AuthPlugin
from httpie.plugins.builtin import HTTPBasicAuth from httpie.plugins.builtin import HTTPBasicAuth
from httpie.plugins.registry import plugin_manager
from httpie.sessions import Session from httpie.sessions import Session
from httpie.utils import get_expired_cookies from httpie.utils import get_expired_cookies
from utils import MockEnvironment, mk_config_dir, http, HTTP_OK from tests.test_auth_plugins import basic_auth
from utils import HTTP_OK, MockEnvironment, http, mk_config_dir
class SessionTestBase: class SessionTestBase:
@ -35,6 +39,27 @@ class SessionTestBase:
return MockEnvironment(config_dir=self.config_dir) return MockEnvironment(config_dir=self.config_dir)
class CookieTestBase:
def setup_method(self, method):
self.config_dir = mk_config_dir()
orig_session = {
'cookies': {
'cookie1': {
'value': 'foo',
},
'cookie2': {
'value': 'foo',
}
}
}
self.session_path = self.config_dir / 'test-session.json'
self.session_path.write_text(json.dumps(orig_session))
def teardown_method(self, method):
shutil.rmtree(self.config_dir)
class TestSessionFlow(SessionTestBase): class TestSessionFlow(SessionTestBase):
""" """
These tests start with an existing session created in `setup_method()`. These tests start with an existing session created in `setup_method()`.
@ -190,14 +215,103 @@ class TestSession(SessionTestBase):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
@pytest.mark.parametrize(
argnames=['auth_require_param', 'auth_parse_param'],
argvalues=[
(False, False),
(False, True),
(True, False)
]
)
def test_auth_type_reused_in_session(self, auth_require_param, auth_parse_param, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
class TestExpiredCookies: header = 'Custom dXNlcjpwYXNzd29yZA'
def setup_method(self, method): class Plugin(AuthPlugin):
auth_type = 'test-reused'
auth_require = auth_require_param
auth_parse = auth_parse_param
def get_auth(self, username=None, password=None):
return basic_auth(header=f'{header}==')
plugin_manager.register(Plugin)
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
'--print=H',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--print=H',
)
assert f'Authorization: {header}' in r1
assert f'Authorization: {header}' in r2
plugin_manager.unregister(Plugin)
@mock.patch('httpie.cli.argtypes.AuthCredentials._getpass',
new=lambda self, prompt: 'password')
def test_auth_plugin_prompt_password_in_session(self, httpbin):
self.start_session(httpbin)
session_path = self.config_dir / 'test-session.json'
class Plugin(AuthPlugin):
auth_type = 'test-prompted'
def get_auth(self, username=None, password=None):
return basic_auth()
plugin_manager.register(Plugin)
r1 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:',
)
r2 = http(
'--session', str(session_path),
httpbin + '/basic-auth/user/password',
)
assert HTTP_OK in r1
assert HTTP_OK in r2
plugin_manager.unregister(Plugin)
def test_auth_type_stored_in_session_file(self, httpbin):
self.config_dir = mk_config_dir() self.config_dir = mk_config_dir()
self.session_path = self.config_dir / 'test-session.json'
def teardown_method(self, method): class Plugin(AuthPlugin):
shutil.rmtree(self.config_dir) auth_type = 'test-saved'
auth_require = True
def get_auth(self, username=None, password=None):
return basic_auth()
plugin_manager.register(Plugin)
http('--session', str(self.session_path),
httpbin + '/basic-auth/user/password',
'--auth-type',
Plugin.auth_type,
'--auth', 'user:password',
)
updated_session = json.loads(self.session_path.read_text())
assert updated_session['auth']['type'] == 'test-saved'
assert updated_session['auth']['raw_auth'] == "user:password"
plugin_manager.unregister(Plugin)
class TestExpiredCookies(CookieTestBase):
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['initial_cookie', 'expired_cookie'], argnames=['initial_cookie', 'expired_cookie'],
@ -213,29 +327,16 @@ class TestExpiredCookies:
assert expired_cookie not in session.cookies assert expired_cookie not in session.cookies
def test_expired_cookies(self, httpbin): 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( r = http(
'--session', str(session_path), '--session', str(self.session_path),
'--print=H', '--print=H',
httpbin.url + '/cookies/delete?to_expire', httpbin.url + '/cookies/delete?cookie2',
) )
assert 'Cookie: to_expire=foo; to_stay=foo' in r assert 'Cookie: cookie1=foo; cookie2=foo' in r
updated_session = json.loads(session_path.read_text()) updated_session = json.loads(self.session_path.read_text())
assert 'to_stay' in updated_session['cookies'] assert 'cookie1' in updated_session['cookies']
assert 'to_expire' not in updated_session['cookies'] assert 'cookie2' not in updated_session['cookies']
@pytest.mark.parametrize( @pytest.mark.parametrize(
argnames=['headers', 'now', 'expected_expired'], argnames=['headers', 'now', 'expected_expired'],
@ -265,6 +366,15 @@ class TestExpiredCookies:
{'name': 'pea', 'path': '/ab'} {'name': 'pea', 'path': '/ab'}
] ]
), ),
(
# Checks we gracefully ignore expires date in invalid format.
# <https://github.com/httpie/httpie/issues/963>
[
('Set-Cookie', 'pfg=; Expires=Sat, 19-Sep-2020 06:58:14 GMT+0000; Max-Age=0; path=/; domain=.tumblr.com; secure; HttpOnly'),
],
None,
[]
),
( (
[ [
('Set-Cookie', 'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly'), ('Set-Cookie', 'hello=world; Path=/; Expires=Fri, 12 Jun 2020 12:28:55 GMT; HttpOnly'),
@ -272,8 +382,95 @@ class TestExpiredCookies:
], ],
datetime(2020, 6, 11).timestamp(), datetime(2020, 6, 11).timestamp(),
[] []
) ),
] ]
) )
def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired): def test_get_expired_cookies_manages_multiple_cookie_headers(self, headers, now, expected_expired):
assert get_expired_cookies(headers, now=now) == expected_expired assert get_expired_cookies(headers, now=now) == expected_expired
class TestCookieStorage(CookieTestBase):
@pytest.mark.parametrize(
argnames=['new_cookies', 'new_cookies_dict', 'expected'],
argvalues=[(
'new=bar',
{'new': 'bar'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar;; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'cookie1=foo; cookie2=foo; new=bar'
),
(
'new=bar; chocolate=milk;;;',
{'new': 'bar', 'chocolate': 'milk'},
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
)
]
)
def test_existing_and_new_cookies_sent_in_request(self, new_cookies, new_cookies_dict, expected, httpbin):
r = http(
'--session', str(self.session_path),
'--print=H',
httpbin.url,
'Cookie:' + new_cookies,
)
# Note: cookies in response are in alphabetical order
assert 'Cookie: ' + expected in r
updated_session = json.loads(self.session_path.read_text())
for name, value in new_cookies_dict.items():
assert name, value in updated_session['cookies']
assert 'Cookie' not in updated_session['headers']
@pytest.mark.parametrize(
argnames=['cli_cookie', 'set_cookie', 'expected'],
argvalues=[(
'',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'/cookies/set/cookie1/bar',
'bar'
),
(
'cookie1=not_foo',
'',
'not_foo'
),
(
'',
'',
'foo'
)
]
)
def test_cookie_storage_priority(self, cli_cookie, set_cookie, expected, httpbin):
"""
Expected order of priority for cookie storage in session file:
1. set-cookie (from server)
2. command line arg
3. cookie already stored in session file
"""
r = http(
'--session', str(self.session_path),
httpbin.url + set_cookie,
'Cookie:' + cli_cookie,
)
updated_session = json.loads(self.session_path.read_text())
assert updated_session['cookies']['cookie1']['value'] == expected

View File

@ -1,6 +1,8 @@
import pytest import pytest
import pytest_httpbin.certs import pytest_httpbin.certs
import requests.exceptions import requests.exceptions
import ssl
import urllib3
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
from httpie.status import ExitStatus from httpie.status import ExitStatus
@ -27,7 +29,6 @@ CLIENT_KEY = str(CERTS_ROOT / 'client.key')
CLIENT_PEM = str(CERTS_ROOT / 'client.pem') CLIENT_PEM = str(CERTS_ROOT / 'client.pem')
# 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.
# See: https://github.com/kevin1024/pytest-httpbin#https-support # See: https://github.com/kevin1024/pytest-httpbin#https-support
@ -47,6 +48,12 @@ def test_ssl_version(httpbin_secure, ssl_version):
if ssl_version == 'ssl3': if ssl_version == 'ssl3':
# pytest-httpbin doesn't support ssl3 # pytest-httpbin doesn't support ssl3
pass pass
elif e.__context__ is not None: # Check if root cause was an unsupported TLS version
root = e.__context__
while root.__context__ is not None:
root = root.__context__
if isinstance(root, ssl.SSLError) and root.reason == "TLSV1_ALERT_PROTOCOL_VERSION":
pytest.skip("Unsupported TLS version: {}".format(ssl_version))
else: else:
raise raise
@ -85,11 +92,14 @@ class TestClientCert:
class TestServerCert: class TestServerCert:
def test_verify_no_OK(self, httpbin_secure): def test_verify_no_OK(self, httpbin_secure):
# Avoid warnings when explicitly testing insecure requests
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
r = http(httpbin_secure.url + '/get', '--verify=no') r = http(httpbin_secure.url + '/get', '--verify=no')
assert HTTP_OK in r assert HTTP_OK in r
@pytest.mark.parametrize('verify_value', ['false', 'fALse']) @pytest.mark.parametrize('verify_value', ['false', 'fALse'])
def test_verify_false_OK(self, httpbin_secure, verify_value): def test_verify_false_OK(self, httpbin_secure, verify_value):
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
r = http(httpbin_secure.url + '/get', '--verify', verify_value) r = http(httpbin_secure.url + '/get', '--verify', verify_value)
assert HTTP_OK in r assert HTTP_OK in r
@ -133,4 +143,9 @@ def test_ciphers_none_can_be_selected(httpbin_secure):
tolerate_error_exit_status=True, tolerate_error_exit_status=True,
) )
assert r.exit_status == ExitStatus.ERROR assert r.exit_status == ExitStatus.ERROR
assert 'No cipher can be selected.' in r.stderr # Linux/macOS:
# http: error: SSLError: ('No cipher can be selected.',)
# OpenBSD:
# <https://marc.info/?l=openbsd-ports&m=159251948515635&w=2>
# http: error: Error: [('SSL routines', '(UNKNOWN)SSL_internal', 'no cipher match')]
assert 'cipher' in r.stderr

View File

@ -2,7 +2,7 @@ import pytest
from httpie.compat import is_windows from httpie.compat import is_windows
from httpie.output.streams import BINARY_SUPPRESSED_NOTICE from httpie.output.streams import BINARY_SUPPRESSED_NOTICE
from utils import http, MockEnvironment from utils import StdinBytesIO, http, MockEnvironment
from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
@ -13,10 +13,12 @@ from fixtures import BIN_FILE_CONTENT, BIN_FILE_PATH
reason='Pretty redirect not supported under Windows') reason='Pretty redirect not supported under Windows')
def test_pretty_redirected_stream(httpbin): def test_pretty_redirected_stream(httpbin):
"""Test that --stream works with prettified redirected output.""" """Test that --stream works with prettified redirected output."""
with open(BIN_FILE_PATH, 'rb') as f: env = MockEnvironment(
env = MockEnvironment(colors=256, stdin=f, colors=256,
stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
stdin_isatty=False, stdin_isatty=False,
stdout_isatty=False) stdout_isatty=False,
)
r = http('--verbose', '--pretty=all', '--stream', 'GET', r = http('--verbose', '--pretty=all', '--stream', 'GET',
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
@ -25,8 +27,10 @@ def test_pretty_redirected_stream(httpbin):
def test_encoded_stream(httpbin): def test_encoded_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: env = MockEnvironment(
env = MockEnvironment(stdin=f, stdin_isatty=False) stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
stdin_isatty=False,
)
r = http('--pretty=none', '--stream', '--verbose', 'GET', r = http('--pretty=none', '--stream', '--verbose', 'GET',
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BINARY_SUPPRESSED_NOTICE.decode() in r assert BINARY_SUPPRESSED_NOTICE.decode() in r
@ -35,10 +39,11 @@ def test_encoded_stream(httpbin):
def test_redirected_stream(httpbin): def test_redirected_stream(httpbin):
"""Test that --stream works with non-prettified """Test that --stream works with non-prettified
redirected terminal output.""" redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f: env = MockEnvironment(
env = MockEnvironment(stdout_isatty=False, stdout_isatty=False,
stdin_isatty=False, stdin_isatty=False,
stdin=f) stdin=StdinBytesIO(BIN_FILE_PATH.read_bytes()),
)
r = http('--pretty=none', '--stream', '--verbose', 'GET', r = http('--pretty=none', '--stream', '--verbose', 'GET',
httpbin.url + '/get', env=env) httpbin.url + '/get', env=env)
assert BIN_FILE_CONTENT in r assert BIN_FILE_CONTENT in r

View File

@ -3,11 +3,55 @@ import os
import pytest import pytest
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
from httpie.client import FORM_CONTENT_TYPE
from httpie.status import ExitStatus from httpie.status import ExitStatus
from utils import MockEnvironment, http, HTTP_OK from utils import (
HTTPBIN_WITH_CHUNKED_SUPPORT, MockEnvironment, StdinBytesIO, http,
HTTP_OK,
)
from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT from fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
def test_chunked_json():
r = http(
'--verbose',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'hello=world',
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count('hello') == 3
def test_chunked_form():
r = http(
'--verbose',
'--chunked',
'--form',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'hello=world',
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count('hello') == 2
def test_chunked_stdin():
r = http(
'--verbose',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
env=MockEnvironment(
stdin=StdinBytesIO(FILE_PATH.read_bytes()),
stdin_isatty=False,
)
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert r.count(FILE_CONTENT) == 2
class TestMultipartFormDataFileUpload: class TestMultipartFormDataFileUpload:
def test_non_existent_file_raises_parse_error(self, httpbin): def test_non_existent_file_raises_parse_error(self, httpbin):
@ -39,15 +83,120 @@ class TestMultipartFormDataFileUpload:
assert r.count('Content-Type: text/plain') == 2 assert r.count('Content-Type: text/plain') == 2
def test_upload_custom_content_type(self, httpbin): def test_upload_custom_content_type(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post', r = http(
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon') '--form',
'--verbose',
httpbin.url + '/post',
f'test-file@{FILE_PATH_ARG};type=image/vnd.microsoft.icon'
)
assert HTTP_OK in r assert HTTP_OK in r
# Content type is stripped from the filename # Content type is stripped from the filename
assert 'Content-Disposition: form-data; name="test-file";' \ assert 'Content-Disposition: form-data; name="test-file";' \
f' filename="{os.path.basename(FILE_PATH)}"' in r f' filename="{os.path.basename(FILE_PATH)}"' in r
assert FILE_CONTENT in r assert r.count(FILE_CONTENT) == 2
assert 'Content-Type: image/vnd.microsoft.icon' in r assert 'Content-Type: image/vnd.microsoft.icon' in r
def test_form_no_files_urlencoded(self, httpbin):
r = http(
'--form',
'--verbose',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert HTTP_OK in r
assert FORM_CONTENT_TYPE in r
def test_multipart(self, httpbin):
r = http(
'--verbose',
'--multipart',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert HTTP_OK in r
assert FORM_CONTENT_TYPE not in r
assert 'multipart/form-data' in r
def test_form_multipart_custom_boundary(self, httpbin):
boundary = 'HTTPIE_FTW'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary}',
httpbin.url + '/post',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/form-data; boundary={boundary}' in r
assert r.count(boundary) == 4
def test_multipart_custom_content_type_boundary_added(self, httpbin):
boundary = 'HTTPIE_FTW'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary}',
httpbin.url + '/post',
'Content-Type: multipart/magic',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/magic; boundary={boundary}' in r
assert r.count(boundary) == 4
def test_multipart_custom_content_type_boundary_preserved(self, httpbin):
# Allow explicit nonsense requests.
boundary_in_header = 'HEADER_BOUNDARY'
boundary_in_body = 'BODY_BOUNDARY'
r = http(
'--print=HB',
'--check-status',
'--multipart',
f'--boundary={boundary_in_body}',
httpbin.url + '/post',
f'Content-Type: multipart/magic; boundary={boundary_in_header}',
'AAAA=AAA',
'BBB=BBB',
)
assert f'multipart/magic; boundary={boundary_in_header}' in r
assert r.count(boundary_in_body) == 3
def test_multipart_chunked(self, httpbin):
r = http(
'--verbose',
'--multipart',
'--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'AAA=AAA',
)
assert 'Transfer-Encoding: chunked' in r
assert 'multipart/form-data' in r
assert 'name="AAA"' in r # in request
assert '"AAA": "AAA"', r # in response
def test_multipart_preserve_order(self, httpbin):
r = http(
'--form',
'--offline',
httpbin + '/post',
'text_field=foo',
f'file_field@{FILE_PATH_ARG}',
)
assert r.index('text_field') < r.index('file_field')
r = http(
'--form',
'--offline',
httpbin + '/post',
f'file_field@{FILE_PATH_ARG}',
'text_field=foo',
)
assert r.index('text_field') > r.index('file_field')
class TestRequestBodyFromFilePath: class TestRequestBodyFromFilePath:
""" """
@ -56,12 +205,26 @@ class TestRequestBodyFromFilePath:
""" """
def test_request_body_from_file_by_path(self, httpbin): def test_request_body_from_file_by_path(self, httpbin):
r = http('--verbose', r = http(
'POST', httpbin.url + '/post', '@' + FILE_PATH_ARG) '--verbose',
'POST', httpbin.url + '/post',
'@' + FILE_PATH_ARG,
)
assert HTTP_OK in r assert HTTP_OK in r
assert FILE_CONTENT in r, r assert r.count(FILE_CONTENT) == 2
assert '"Content-Type": "text/plain"' in r assert '"Content-Type": "text/plain"' in r
def test_request_body_from_file_by_path_chunked(self, httpbin):
r = http(
'--verbose', '--chunked',
HTTPBIN_WITH_CHUNKED_SUPPORT + '/post',
'@' + FILE_PATH_ARG,
)
assert HTTP_OK in r
assert 'Transfer-Encoding: chunked' in r
assert '"Content-Type": "text/plain"' in r
assert r.count(FILE_CONTENT) == 2
def test_request_body_from_file_by_path_with_explicit_content_type( def test_request_body_from_file_by_path_with_explicit_content_type(
self, httpbin): self, httpbin):
r = http('--verbose', r = http('--verbose',

View File

@ -1,10 +1,10 @@
# coding=utf-8 # coding=utf-8
"""Utilities for HTTPie test suite.""" """Utilities for HTTPie test suite."""
import os
import sys import sys
import time import time
import json import json
import tempfile import tempfile
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Optional, Union from typing import Optional, Union
@ -14,6 +14,12 @@ from httpie.context import Environment
from httpie.core import main from httpie.core import main
# pytest-httpbin currently does not support chunked requests:
# <https://github.com/kevin1024/pytest-httpbin/issues/33>
# <https://github.com/kevin1024/pytest-httpbin/issues/28>
HTTPBIN_WITH_CHUNKED_SUPPORT = 'http://httpbin.org'
TESTS_ROOT = Path(__file__).parent TESTS_ROOT = Path(__file__).parent
CRLF = '\r\n' CRLF = '\r\n'
COLOR = '\x1b[' COLOR = '\x1b['
@ -36,6 +42,11 @@ def add_auth(url, auth):
return proto + '://' + auth + '@' + rest return proto + '://' + auth + '@' + rest
class StdinBytesIO(BytesIO):
"""To be used for `MockEnvironment.stdin`"""
len = 0 # See `prepare_request_body()`
class MockEnvironment(Environment): class MockEnvironment(Environment):
"""Environment subclass with reasonable defaults for testing.""" """Environment subclass with reasonable defaults for testing."""
colors = 0 colors = 0
@ -94,10 +105,12 @@ class BaseCLIResponse:
- stdout output: print(self) - stdout output: print(self)
- stderr output: print(self.stderr) - stderr output: print(self.stderr)
- devnull output: print(self.devnull)
- exit_status output: print(self.exit_status) - exit_status output: print(self.exit_status)
""" """
stderr: str = None stderr: str = None
devnull: str = None
json: dict = None json: dict = None
exit_status: ExitStatus = None exit_status: ExitStatus = None
@ -131,10 +144,9 @@ class StrCLIResponse(str, BaseCLIResponse):
elif self.strip().startswith('{'): elif self.strip().startswith('{'):
# Looks like JSON body. # Looks like JSON body.
self._json = json.loads(self) self._json = json.loads(self)
elif (self.count('Content-Type:') == 1 elif self.count('Content-Type:') == 1:
and 'application/json' in self): # Looks like a HTTP message,
# Looks like a whole JSON HTTP message, # try to extract JSON from its body.
# try to extract its body.
try: try:
j = self.strip()[self.strip().rindex('\r\n\r\n'):] j = self.strip()[self.strip().rindex('\r\n\r\n'):]
except ValueError: except ValueError:
@ -161,17 +173,21 @@ def http(
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
""" """
Run HTTPie and capture stderr/out and exit status. Run HTTPie and capture stderr/out and exit status.
Content writtent to devnull will be captured only if
env.devnull is set manually.
Invoke `httpie.core.main()` with `args` and `kwargs`, Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a `CLIResponse` subclass instance. and return a `CLIResponse` subclass instance.
The return value is either a `StrCLIResponse`, or `BytesCLIResponse` The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output. if unable to decode the output. Devnull is string when possible,
bytes otherwise.
The response has the following attributes: The response has the following attributes:
`stdout` is represented by the instance itself (print r) `stdout` is represented by the instance itself (print r)
`stderr`: text written to stderr `stderr`: text written to stderr
`devnull` text written to devnull.
`exit_status`: the exit status `exit_status`: the exit status
`json`: decoded JSON (if possible) or `None` `json`: decoded JSON (if possible) or `None`
@ -204,6 +220,7 @@ def http(
stdout = env.stdout stdout = env.stdout
stderr = env.stderr stderr = env.stderr
devnull = env.devnull
args = list(args) args = list(args)
args_with_config_defaults = args + env.config.default_options args_with_config_defaults = args + env.config.default_options
@ -249,13 +266,22 @@ def http(
stdout.seek(0) stdout.seek(0)
stderr.seek(0) stderr.seek(0)
devnull.seek(0)
output = stdout.read() output = stdout.read()
devnull_output = devnull.read()
try: try:
output = output.decode('utf8') output = output.decode('utf8')
except UnicodeDecodeError: except UnicodeDecodeError:
r = BytesCLIResponse(output) r = BytesCLIResponse(output)
else: else:
r = StrCLIResponse(output) r = StrCLIResponse(output)
try:
devnull_output = devnull_output.decode('utf8')
except Exception:
pass
r.devnull = devnull_output
r.stderr = stderr.read() r.stderr = stderr.read()
r.exit_status = exit_status r.exit_status = exit_status
@ -265,6 +291,7 @@ def http(
return r return r
finally: finally:
devnull.close()
stdout.close() stdout.close()
stderr.close() stderr.close()
env.cleanup() env.cleanup()

23
tox.ini
View File

@ -1,23 +0,0 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. See ./CONTRIBUTING.rst
[tox]
# pypy3 currently fails because of a Flask issue
envlist = py37
[testenv]
deps =
mock
pytest
pytest-httpbin>=0.0.6
commands =
# NOTE: the order of the directories in posargs seems to matter.
# When changed, then many ImportMismatchError exceptions occurrs.
py.test \
--verbose \
--doctest-modules \
{posargs:./httpie ./tests}