mirror of
https://github.com/httpie/cli.git
synced 2025-08-14 13:29:18 +02:00
Compare commits
61 Commits
auto-updat
...
3.2.2
Author | SHA1 | Date | |
---|---|---|---|
29de4ce115 | |||
879fedc10a | |||
18bb49b268 | |||
fcd3f7ece6 | |||
8e56e9fc64 | |||
44d3cff03f | |||
d021b94b5d | |||
4e29a6d561 | |||
1ae4152e1e | |||
47e9b99ba1 | |||
265841f866 | |||
b16392fbb9 | |||
e73c3e6c24 | |||
f0563deb7f | |||
4894b4c0fc | |||
3a123c4125 | |||
621042a048 | |||
0689b55e1d | |||
a7321d8ac4 | |||
d9a73cd8eb | |||
930cd9081a | |||
3549ee8342 | |||
810bb1c77b | |||
767f3c3a19 | |||
1236793272 | |||
c3a2f87dd2 | |||
1121d695a8 | |||
5794a070e1 | |||
4736a16698 | |||
3ad408add7 | |||
91cdb22a4b | |||
c995fd9b24 | |||
418b12bbd6 | |||
ecff53f2d5 | |||
41da87f7c8 | |||
4f172a61b4 | |||
542a2d35de | |||
d9e1dc08c9 | |||
3b734fb0bc | |||
8abe47969e | |||
8173cb0337 | |||
7fd34fc8ce | |||
80ae644464 | |||
69fe5dbfd1 | |||
f09e7564e7 | |||
dc5274e491 | |||
ad2b86ccf4 | |||
11b2af0f59 | |||
b54239b525 | |||
b0b0f3dc53 | |||
9f7612cdeb | |||
5e76ebc5e1 | |||
343a521673 | |||
2142ae60c3 | |||
0b6a9b23c2 | |||
9e1c0b98c7 | |||
003f2095d4 | |||
f9b5c2f696 | |||
76495cbdec | |||
c4d7d05f3b | |||
7a4fb5d966 |
2
.github/workflows/autogenerated-files.yml
vendored
2
.github/workflows/autogenerated-files.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
|
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
|
2
.github/workflows/code-style.yml
vendored
2
.github/workflows/code-style.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make venv
|
||||
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- run: make install
|
||||
|
2
.github/workflows/release-brew.yml
vendored
2
.github/workflows/release-brew.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: mislav/bump-homebrew-formula-action@v1
|
||||
- uses: mislav/bump-homebrew-formula-action@v2
|
||||
with:
|
||||
formula-name: httpie
|
||||
tag-name: ${{ github.events.inputs.branch }}
|
||||
|
17
.github/workflows/release-choco.yml
vendored
17
.github/workflows/release-choco.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
name: Release the Chocolatey
|
||||
runs-on: windows-2019
|
||||
env:
|
||||
package-dir: ./httpie/docs/packaging/windows-chocolatey
|
||||
package-dir: docs\packaging\windows-chocolatey
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -30,9 +30,21 @@ jobs:
|
||||
run: choco info httpie -s .
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
||||
- name: Check the Installation
|
||||
- name: Local installation
|
||||
run: |
|
||||
choco install httpie -y -dv -s "'.;https://community.chocolatey.org/api/v2/'"
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
||||
- name: Test the locally installed binaries
|
||||
run: |
|
||||
# Source: https://stackoverflow.com/a/46760714/15330941
|
||||
|
||||
# Make `refreshenv` available right away, by defining the $env:ChocolateyInstall
|
||||
# variable and importing the Chocolatey profile module.
|
||||
$env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
|
||||
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
||||
refreshenv
|
||||
|
||||
http --version
|
||||
https --version
|
||||
httpie --version
|
||||
@ -46,3 +58,4 @@ jobs:
|
||||
run: |
|
||||
choco apikey --key $CHOCO_API_KEY --source https://push.chocolatey.org/
|
||||
choco push httpie*.nupkg --source https://push.chocolatey.org/
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
23
.github/workflows/release-linux-standalone.yml
vendored
23
.github/workflows/release-linux-standalone.yml
vendored
@ -7,6 +7,9 @@ on:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
tag_name:
|
||||
description: "Which release to upload the artifacts to (e.g., 3.0)"
|
||||
required: true
|
||||
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
@ -21,7 +24,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
@ -45,24 +48,30 @@ jobs:
|
||||
name: httpie.rpm
|
||||
path: extras/packaging/linux/artifacts/dist/*.rpm
|
||||
|
||||
- name: Determine the release upload upload_url
|
||||
id: release_id
|
||||
run: |
|
||||
pip install httpie
|
||||
export API_URL="api.github.com/repos/httpie/httpie/releases/tags/${{ github.event.inputs.tag_name }}"
|
||||
export UPLOAD_URL=`https --ignore-stdin GET $API_URL | jq -r ".upload_url"`
|
||||
echo "::set-output name=UPLOAD_URL::$UPLOAD_URL"
|
||||
|
||||
- name: Publish Debian Package
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: extras/packaging/linux/artifacts/dist/httpie-${{ github.event.release.tag_name }}.deb
|
||||
asset_name: httpie-${{ github.event.release.tag_name }}.deb
|
||||
upload_url: ${{ steps.release_id.outputs.UPLOAD_URL }}
|
||||
asset_path: extras/packaging/linux/artifacts/dist/httpie_${{ github.event.inputs.tag_name }}_amd64.deb
|
||||
asset_name: httpie-${{ github.event.inputs.tag_name }}.deb
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
- name: Publish Single Executable
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
upload_url: ${{ steps.release_id.outputs.UPLOAD_URL }}
|
||||
asset_path: extras/packaging/linux/artifacts/dist/http
|
||||
asset_name: http
|
||||
asset_content_type: binary/octet-stream
|
||||
|
7
.github/workflows/release-pypi.yml
vendored
7
.github/workflows/release-pypi.yml
vendored
@ -17,15 +17,12 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install pypa/build
|
||||
run: python -m pip install build
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: python -m build --sdist --wheel --outdir dist/
|
||||
run: make install && make build
|
||||
|
||||
- name: Release on PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
close-pr-message: 'Thanks for the pull request, but since it was stale for more than a 30 days we are closing it. If you want to work back on it, feel free to re-open it or create a new one.'
|
||||
stale-pr-label: 'stale'
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Windows setup
|
||||
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -3,14 +3,28 @@
|
||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [3.1.1.dev0](https://github.com/httpie/httpie/compare/3.1.0...HEAD) (Unreleased)
|
||||
## [3.2.2](https://github.com/httpie/httpie/compare/3.2.1...3.2.2) (2022-05-19)
|
||||
|
||||
- Fixed compatibility with urllib3 2.0.0. ([#1499](https://github.com/httpie/httpie/issue/1499))
|
||||
|
||||
## [3.2.1](https://github.com/httpie/httpie/compare/3.1.0...3.2.1) (2022-05-06)
|
||||
|
||||
- Improved support for determining auto-streaming when the `Content-Type` header includes encoding information. ([#1383](https://github.com/httpie/httpie/pull/1383))
|
||||
- Fixed the display of the crash happening in the secondary process for update checks. ([#1388](https://github.com/httpie/httpie/issues/1388))
|
||||
|
||||
## [3.2.0](https://github.com/httpie/httpie/compare/3.1.0...3.2.0) (2022-05-05)
|
||||
|
||||
- Added a warning for notifying the user about the new updates. ([#1336](https://github.com/httpie/httpie/pull/1336))
|
||||
- Added support for single binary executables. ([#1330](https://github.com/httpie/httpie/pull/1330))
|
||||
- Added support for man pages (and auto generation of them from the parser declaration). ([#1317](https://github.com/httpie/httpie/pull/1317))
|
||||
- Added `http --manual` for man pages & regular manual with pager. ([#1343](https://github.com/httpie/httpie/pull/1343))
|
||||
- Added support for session persistence of repeated headers with the same name. ([#1335](https://github.com/httpie/httpie/pull/1335))
|
||||
- Changed `httpie plugins` to the new `httpie cli` namespace as `httpie cli plugins` (`httpie plugins` continues to work as a hidden alias). ([#1320](https://github.com/httpie/httpie/issues/1320))
|
||||
- Added support for sending `Secure` cookies to the `localhost` (and `.local` suffixed domains). ([#1308](https://github.com/httpie/httpie/issues/1308))
|
||||
- Improved UI for the progress bars. ([#1324](https://github.com/httpie/httpie/pull/1324))
|
||||
- Fixed redundant creation of `Content-Length` header on `OPTIONS` requests. ([#1310](https://github.com/httpie/httpie/issues/1310))
|
||||
- Fixed blocking of warning thread on some use cases. ([#1349](https://github.com/httpie/httpie/issues/1349))
|
||||
- Added support for sending `Secure` cookies to the `localhost` (and `.local` suffixed domains). ([#1308](https://github.com/httpie/httpie/issues/1308))
|
||||
|
||||
- Changed `httpie plugins` to the new `httpie cli` namespace as `httpie cli plugins` (`httpie plugins` continues to work as a hidden alias). ([#1320](https://github.com/httpie/httpie/issues/1320))
|
||||
- Soft deprecated the `--history-print`. ([#1380](https://github.com/httpie/httpie/pull/1380))
|
||||
|
||||
## [3.1.0](https://github.com/httpie/httpie/compare/3.0.2...3.1.0) (2022-03-08)
|
||||
|
||||
|
@ -59,8 +59,10 @@ $ git checkout -b my_topical_branch
|
||||
|
||||
#### Setup
|
||||
|
||||
The [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) contains a bunch of tasks to get you started. Just run
|
||||
the following command, which:
|
||||
The [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) contains a bunch of tasks to get you started.
|
||||
You can run `$ make` to see all the available tasks.
|
||||
|
||||
To get started, run the command below, which:
|
||||
|
||||
- Creates an isolated Python virtual environment inside `./venv`
|
||||
(via the standard library [venv](https://docs.python.org/3/library/venv.html) tool);
|
||||
@ -70,7 +72,7 @@ the following command, which:
|
||||
- and runs tests (It is the same as running `make install test`).
|
||||
|
||||
```bash
|
||||
$ make
|
||||
$ make all
|
||||
```
|
||||
|
||||
#### Python virtual environment
|
||||
@ -152,7 +154,7 @@ with the master branch of your repository (or a fresh checkout of HTTPie master,
|
||||
`--fresh`) and report the results back.
|
||||
|
||||
```bash
|
||||
$ python extras/benchmarks/run.py
|
||||
$ python extras/profiling/run.py
|
||||
```
|
||||
|
||||
The benchmarks can also be run on the CI. Since it is a long process, it requires manual
|
||||
|
33
Makefile
33
Makefile
@ -22,6 +22,26 @@ VENV_PYTHON=$(VENV_BIN)/python
|
||||
export PATH := $(VENV_BIN):$(PATH)
|
||||
|
||||
|
||||
|
||||
default: list-tasks
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Default task to get a list of tasks when `make' is run without args.
|
||||
# <https://stackoverflow.com/questions/4219255>
|
||||
###############################################################################
|
||||
|
||||
list-tasks:
|
||||
@echo Available tasks:
|
||||
@echo ----------------
|
||||
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | grep -E -v -e '^[^[:alnum:]]' -e '^$@$$'
|
||||
@echo
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Installation
|
||||
###############################################################################
|
||||
|
||||
all: uninstall-httpie install test
|
||||
|
||||
|
||||
@ -30,10 +50,10 @@ install: venv install-reqs
|
||||
|
||||
install-reqs:
|
||||
@echo $(H1)Updating package tools$(H1END)
|
||||
$(VENV_PIP) install --upgrade pip wheel
|
||||
$(VENV_PIP) install --upgrade pip wheel build
|
||||
|
||||
@echo $(H1)Installing dev requirements$(H1END)
|
||||
$(VENV_PIP) install --upgrade --editable '.[dev]'
|
||||
$(VENV_PIP) install --upgrade '.[dev]' '.[test]'
|
||||
|
||||
@echo $(H1)Installing HTTPie$(H1END)
|
||||
$(VENV_PIP) install --upgrade --editable .
|
||||
@ -153,8 +173,11 @@ doc-check:
|
||||
|
||||
|
||||
build:
|
||||
rm -rf build/
|
||||
$(VENV_PYTHON) setup.py sdist bdist_wheel
|
||||
rm -rf build/ dist/
|
||||
mv httpie/internal/__build_channel__.py httpie/internal/__build_channel__.py.original
|
||||
echo 'BUILD_CHANNEL = "pip"' > httpie/internal/__build_channel__.py
|
||||
$(VENV_PYTHON) -m build --sdist --wheel --outdir dist/
|
||||
mv httpie/internal/__build_channel__.py.original httpie/internal/__build_channel__.py
|
||||
|
||||
|
||||
publish: test-all publish-no-test
|
||||
@ -198,7 +221,7 @@ brew-test:
|
||||
- brew uninstall httpie
|
||||
|
||||
@echo $(H1)Building from source…$(H1END)
|
||||
- brew install --build-from-source ./docs/packaging/brew/httpie.rb
|
||||
- brew install --HEAD --build-from-source ./docs/packaging/brew/httpie.rb
|
||||
|
||||
@echo $(H1)Verifying…$(H1END)
|
||||
http --version
|
||||
|
50
README.md
50
README.md
@ -1,10 +1,30 @@
|
||||
<br/>
|
||||
<a href="https://httpie.io" target="blank_">
|
||||
<h2 align="center">
|
||||
<a href="https://httpie.io" target="blank_">
|
||||
<img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" />
|
||||
</a>
|
||||
<br/>
|
||||
</a>
|
||||
<br>
|
||||
HTTPie for Terminal: human-friendly CLI HTTP client for the API era
|
||||
</h2>
|
||||
|
||||
# HTTPie: human-friendly CLI HTTP client for the API era
|
||||
<div align="center">
|
||||
|
||||
[](https://httpie.io/product)
|
||||
[](https://httpie.io/app)
|
||||
[](https://httpie.io/cli)
|
||||
[](https://twitter.com/httpie)
|
||||
[](https://httpie.io/discord)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://httpie.org/docs/cli)
|
||||
[](https://pypi.python.org/pypi/httpie)
|
||||
[](https://github.com/httpie/httpie/actions)
|
||||
[](https://codecov.io/gh/httpie/httpie)
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
@ -12,16 +32,16 @@ HTTPie is designed for testing, debugging, and generally interacting with APIs &
|
||||
The `http` & `https` commands allow for creating and sending arbitrary HTTP requests.
|
||||
They use simple and natural syntax and provide formatted and colorized output.
|
||||
|
||||
[](https://httpie.org/docs)
|
||||
[](https://pypi.python.org/pypi/httpie)
|
||||
[](https://github.com/httpie/httpie/actions)
|
||||
[](https://codecov.io/gh/httpie/httpie)
|
||||
[](https://twitter.com/httpie)
|
||||
[](https://httpie.io/discord)
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
## We lost 54k GitHub stars
|
||||
|
||||
Please note we recently accidentally made this repo private for a moment, and GitHub deleted our community that took a decade to build. Read the full story here: https://httpie.io/blog/stardust
|
||||
@ -53,25 +73,25 @@ Please note we recently accidentally made this repo private for a moment, and Gi
|
||||
Hello World:
|
||||
|
||||
```bash
|
||||
$ https httpie.io/hello
|
||||
https httpie.io/hello
|
||||
```
|
||||
|
||||
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
|
||||
|
||||
```bash
|
||||
$ http PUT pie.dev/put X-API-Token:123 name=John
|
||||
http PUT pie.dev/put X-API-Token:123 name=John
|
||||
```
|
||||
|
||||
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
|
||||
|
||||
```bash
|
||||
$ http --offline pie.dev/post hello=offline
|
||||
http --offline pie.dev/post hello=offline
|
||||
```
|
||||
|
||||
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
|
||||
|
||||
```bash
|
||||
$ http -a USERNAME POST https://api.github.com/repos/httpie/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:'
|
||||
```
|
||||
|
||||
[See more examples →](https://httpie.io/docs#examples)
|
||||
|
@ -162,6 +162,8 @@ Also works for other Debian-derived distributions like MX Linux, Linux Mint, dee
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ curl -SsL https://packages.httpie.io/deb/KEY.gpg | apt-key add -
|
||||
$ curl -SsL -o /etc/apt/sources.list.d/httpie.list https://packages.httpie.io/deb/httpie.list
|
||||
$ apt update
|
||||
$ apt install httpie
|
||||
```
|
||||
@ -213,6 +215,21 @@ $ pacman -Syu httpie
|
||||
$ pacman -Syu
|
||||
```
|
||||
|
||||
#### Single binary executables
|
||||
|
||||
Get the standalone HTTPie Linux executables when you don't want to go through the full installation process
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ https --download packages.httpie.io/binaries/linux/http-latest -o http
|
||||
$ chmod +x ./http
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ https --download packages.httpie.io/binaries/linux/http-latest -o http
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
#### FreshPorts
|
||||
@ -233,36 +250,39 @@ $ pkg upgrade www/py-httpie
|
||||
|
||||
### Unstable version
|
||||
|
||||
You can also install the latest unreleased development version directly from the `master` branch on GitHub.
|
||||
It is a work-in-progress of a future stable release so the experience might be not as smooth.
|
||||
If you want to try out the latest version of HTTPie that hasn't been officially released yet, you can install the development or unstable version directly from the master branch on GitHub. However, keep in mind that the development version is a work in progress and may not be as reliable as the stable version.
|
||||
|
||||
You can install it on Linux, macOS, Windows, or FreeBSD with `pip`:
|
||||
You can use the following command to install the development version of HTTPie on Linux, macOS, Windows, or FreeBSD operating systems. With this command, the code present in the `master` branch is downloaded and installed using `pip`.
|
||||
|
||||
```bash
|
||||
$ python -m pip install --upgrade https://github.com/httpie/httpie/archive/master.tar.gz
|
||||
```
|
||||
|
||||
Or on macOS, and Linux, with Homebrew:
|
||||
There are other ways to install the development version of HTTPie on macOS and Linux.
|
||||
|
||||
You can install it using Homebrew by running the following commands:
|
||||
|
||||
```bash
|
||||
$ brew uninstall --force httpie
|
||||
$ brew install --HEAD httpie
|
||||
```
|
||||
|
||||
And even on macOS, and Linux, with Snapcraft:
|
||||
You can install it using Snapcraft by running the following commands:
|
||||
|
||||
```bash
|
||||
$ snap remove httpie
|
||||
$ snap install httpie --edge
|
||||
```
|
||||
|
||||
Verify that now you have the [current development version identifier](https://github.com/httpie/httpie/blob/master/httpie/__init__.py#L6) with the `.dev0` suffix, for example:
|
||||
To verify the installation, you can compare the [version identifier on GitHub](https://github.com/httpie/httpie/blob/master/httpie/__init__.py#L6) with the one available on your machine. You can check the version of HTTPie on your machine by using the command `http --version`.
|
||||
|
||||
```bash
|
||||
$ http --version
|
||||
# 3.X.X.dev0
|
||||
```
|
||||
|
||||
Note that on your machine, the version name will have the `.dev0` suffix.
|
||||
|
||||
## Usage
|
||||
|
||||
Hello World:
|
||||
@ -277,7 +297,7 @@ Synopsis:
|
||||
$ http [flags] [METHOD] URL [ITEM [ITEM]]
|
||||
```
|
||||
|
||||
See also `http --help`.
|
||||
See also `http --help` (and for systems where man pages are available, you can use `man http`).
|
||||
|
||||
### Examples
|
||||
|
||||
@ -1436,7 +1456,8 @@ $ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
|
||||
## HTTPS
|
||||
|
||||
### Server SSL certificate verification
|
||||
To skip the host’s SSL certificate verification, you can pass `--verify=no` (default is `yes`):
|
||||
|
||||
To skip the host’s SSL certificate verification, you can pass `--verify=no` (default is `yes`):
|
||||
|
||||
```bash
|
||||
$ http --verify=no https://pie.dev/get
|
||||
@ -1653,6 +1674,10 @@ If you’d like to silence warnings as well, use `-q` or `--quiet` twice:
|
||||
Let’s say that there is an API that returns the whole resource when it is updated, but you are only interested in the response headers to see the status code after an update:
|
||||
|
||||
```bash
|
||||
$ http --headers PATCH pie.dev/patch name='New Name'
|
||||
```
|
||||
|
||||
Since you are only printing the HTTP headers here, the connection to the server is closed as soon as all the response headers have been received.
|
||||
Therefore, bandwidth and time isn’t wasted downloading the body which you don’t care about.
|
||||
The response headers are downloaded always, even if they are not part of the output
|
||||
|
||||
@ -2373,12 +2398,9 @@ fi
|
||||
You can check whether a new update is available for your system by running `httpie cli check-updates`:
|
||||
|
||||
```bash-termible
|
||||
`httpie cli export-args` command can expose the parser specification of `http`/`https` commands
|
||||
(like an API definition) to outside tools so that they can use this to build better interactions
|
||||
over them (e.g., offer auto-complete).
|
||||
|
||||
$ httpie cli check-updates
|
||||
```
|
||||
|
||||
|
||||
#### `httpie cli export-args`
|
||||
|
||||
`httpie cli export-args` command can expose the parser specification of `http`/`https` commands
|
||||
@ -2398,6 +2420,14 @@ This command is currently in beta.
|
||||
"Program: http, Version: 0.0.1a0"
|
||||
```
|
||||
|
||||
#### `httpie cli plugins`
|
||||
|
||||
`plugins` interface is a very simple plugin manager for installing, listing and uninstalling HTTPie plugins.
|
||||
|
||||
In the past `pip` was used to install/uninstall plugins, but on some environments (e.g., brew installed
|
||||
packages) it wasn’t working properly. The new interface is a very simple overlay on top of `pip` to allow
|
||||
plugin installations on every installation method.
|
||||
|
||||
By default, the plugins (and their missing dependencies) will be stored under the configuration directory,
|
||||
but this can be modified through `plugins_dir` variable on the config.
|
||||
|
||||
@ -2524,7 +2554,7 @@ HTTPie has the following community channels:
|
||||
|
||||
See [github.com/httpie/httpie/security/policy](https://github.com/httpie/httpie/security/policy).
|
||||
|
||||
### Change log
|
||||
### Change log
|
||||
|
||||
See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md).
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
Here we maintain a database of contributors, from which we generate credits on release blog posts and social medias.
|
||||
Here we maintain a database of contributors, from which we generate credits on release blog posts and social media.
|
||||
|
||||
For the HTTPie blog see: <https://httpie.io/blog>.
|
||||
|
@ -252,6 +252,7 @@ def fetch_missing_users_details(people: People) -> None:
|
||||
def save_awesome_people(people: People) -> None:
|
||||
with DB_FILE.open(mode='w', encoding='utf-8') as fh:
|
||||
json.dump(people, fh, indent=4, sort_keys=True)
|
||||
fh.write("\n")
|
||||
|
||||
|
||||
def debug(*args: Any) -> None:
|
||||
|
@ -8,19 +8,27 @@ from jinja2 import Template
|
||||
from fetch import HERE, load_awesome_people
|
||||
|
||||
TPL_FILE = HERE / 'snippet.jinja2'
|
||||
|
||||
HTTPIE_TEAM = {
|
||||
'claudiatd',
|
||||
'jakubroztocil',
|
||||
'jkbr',
|
||||
'isidentical'
|
||||
}
|
||||
|
||||
BOT_ACCOUNTS = {
|
||||
'dependabot-sr'
|
||||
}
|
||||
|
||||
IGNORE_ACCOUNTS = HTTPIE_TEAM | BOT_ACCOUNTS
|
||||
|
||||
|
||||
def generate_snippets(release: str) -> str:
|
||||
people = load_awesome_people()
|
||||
contributors = {
|
||||
name: details
|
||||
for name, details in people.items()
|
||||
if details['github'] not in HTTPIE_TEAM
|
||||
if details['github'] not in IGNORE_ACCOUNTS
|
||||
and (release in details['committed'] or release in details['reported'])
|
||||
}
|
||||
|
||||
|
@ -53,11 +53,13 @@
|
||||
},
|
||||
"Batuhan Taskaya": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "isidentical",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": "isidentical"
|
||||
},
|
||||
@ -118,6 +120,14 @@
|
||||
"reported": [],
|
||||
"twitter": "elena_lape"
|
||||
},
|
||||
"Ethan Mills": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "ethanmills",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Fabio Peruzzo": {
|
||||
"committed": [],
|
||||
"github": "peruzzof",
|
||||
@ -189,7 +199,8 @@
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "jakubroztocil",
|
||||
"reported": [
|
||||
@ -213,7 +224,8 @@
|
||||
],
|
||||
"github": "blyxxyz",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -309,7 +321,8 @@
|
||||
"committed": [],
|
||||
"github": "ducaale",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -321,6 +334,22 @@
|
||||
],
|
||||
"twitter": "sevenc_nanashi"
|
||||
},
|
||||
"Nicklas Ansman Giertz": {
|
||||
"committed": [],
|
||||
"github": "ansman",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Oliver Fish": {
|
||||
"committed": [],
|
||||
"github": "Oliver-Fish",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Omer Akram": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
@ -357,6 +386,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Roberto L\u00f3pez L\u00f3pez": {
|
||||
"committed": [],
|
||||
"github": "robertolopezlopez",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Russell Shurts": {
|
||||
"committed": [],
|
||||
"github": "rshurts",
|
||||
@ -487,6 +524,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"dependabot[bot]": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "dependabot-sr",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"dkreeft": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
@ -553,6 +598,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"luzpaz": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "luzpaz",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"nixbytes": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -593,6 +646,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zhaohanqing95": {
|
||||
"committed": [],
|
||||
"github": "zhaohanqing95",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zoulja": {
|
||||
"committed": [],
|
||||
"github": "zoulja",
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
## Community contributions
|
||||
|
||||
We’d like to thank these amazing people for their contributions to this release: {% for name, details in contributors.items() -%}
|
||||
[{{ name }}](https://github.com/{{ details.github }}){{ '' if loop.last else ', ' }}
|
||||
{%- endfor %}.
|
||||
We’d like to thank these amazing people for their contributions to this release:
|
||||
{% for name, details in contributors.items() -%}
|
||||
- [{{ name }}](https://github.com/{{ details.github }}){{ '' if loop.last else '\n' }}
|
||||
{%- endfor %}
|
||||
|
||||
<!-- Twitter -->
|
||||
|
||||
|
@ -55,7 +55,7 @@ def build_docs_structure(database: Database):
|
||||
tree = database[KEY_DOC_STRUCTURE]
|
||||
structure = []
|
||||
for platform, tools_ids in tree.items():
|
||||
assert platform.isalnum(), f'{platform=} must be alpha-numeric for generated links to work'
|
||||
assert platform.isalnum(), f'{platform=} must be alphanumeric for generated links to work'
|
||||
platform_tools = [tools[tool_id] for tool_id in tools_ids]
|
||||
structure.append((platform, platform_tools))
|
||||
return structure
|
||||
|
@ -17,11 +17,12 @@ docs-structure:
|
||||
Windows:
|
||||
- chocolatey
|
||||
Linux:
|
||||
- snap-linux
|
||||
- brew-linux
|
||||
- apt
|
||||
- dnf
|
||||
- yum
|
||||
- single-binary
|
||||
- snap-linux
|
||||
- brew-linux
|
||||
- pacman
|
||||
FreeBSD:
|
||||
- pkg
|
||||
@ -36,6 +37,8 @@ tools:
|
||||
package: https://packages.debian.org/sid/web/httpie
|
||||
commands:
|
||||
install:
|
||||
- curl -SsL https://packages.httpie.io/deb/KEY.gpg | apt-key add -
|
||||
- curl -SsL -o /etc/apt/sources.list.d/httpie.list https://packages.httpie.io/deb/httpie.list
|
||||
- apt update
|
||||
- apt install httpie
|
||||
upgrade:
|
||||
@ -179,3 +182,16 @@ tools:
|
||||
- yum install httpie
|
||||
upgrade:
|
||||
- yum upgrade httpie
|
||||
|
||||
single-binary:
|
||||
title: Single binary executables
|
||||
name: Single binary executables
|
||||
note: Get the standalone HTTPie Linux executables when you don't want to go through the full installation process.
|
||||
links:
|
||||
commands:
|
||||
install:
|
||||
- https --download packages.httpie.io/binaries/linux/http-latest -o http
|
||||
- ln -ls ./http ./https
|
||||
- chmod +x ./http ./https
|
||||
upgrade:
|
||||
- https --download packages.httpie.io/binaries/linux/http-latest -o http
|
||||
|
@ -22,7 +22,7 @@ If it is needed to be done manually, the following command can be used:
|
||||
$ brew bump-formula-pr httpie --version={TARGET_VERSION}
|
||||
```
|
||||
|
||||
which will bump the formala, and create a PR against the package index.
|
||||
which will bump the formula, and create a PR against the package index.
|
||||
|
||||
## Hacking
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>httpie</id>
|
||||
<version>3.1.0</version>
|
||||
<version>3.2.1</version>
|
||||
<summary>Modern, user-friendly command-line HTTP client for the API era</summary>
|
||||
<description>
|
||||
HTTPie *aitch-tee-tee-pie* is a user-friendly command-line HTTP client for the API era.
|
||||
@ -33,7 +33,7 @@ Main features:
|
||||
<licenseUrl>https://raw.githubusercontent.com/httpie/httpie/master/LICENSE</licenseUrl>
|
||||
<iconUrl>https://pie-assets.s3.eu-central-1.amazonaws.com/LogoIcons/GB.png</iconUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<releaseNotes>See the [changelog](https://github.com/httpie/httpie/releases/tag/3.1.0).</releaseNotes>
|
||||
<releaseNotes>See the [changelog](https://github.com/httpie/httpie/releases/tag/3.2.0).</releaseNotes>
|
||||
<tags>httpie http https rest api client curl python ssl cli foss oss url</tags>
|
||||
<projectUrl>https://httpie.io</projectUrl>
|
||||
<packageSourceUrl>https://github.com/httpie/httpie/tree/master/docs/packaging/windows-chocolatey</packageSourceUrl>
|
||||
|
@ -1,52 +1,24 @@
|
||||
function __fish_httpie_styles
|
||||
echo "
|
||||
abap
|
||||
algol
|
||||
algol_nu
|
||||
arduino
|
||||
auto
|
||||
autumn
|
||||
borland
|
||||
bw
|
||||
colorful
|
||||
default
|
||||
emacs
|
||||
friendly
|
||||
fruity
|
||||
gruvbox-dark
|
||||
gruvbox-light
|
||||
igor
|
||||
inkpot
|
||||
lovelace
|
||||
manni
|
||||
material
|
||||
monokai
|
||||
murphy
|
||||
native
|
||||
paraiso-dark
|
||||
paraiso-light
|
||||
pastie
|
||||
perldoc
|
||||
rainbow_dash
|
||||
rrt
|
||||
sas
|
||||
solarized
|
||||
solarized-dark
|
||||
solarized-light
|
||||
stata
|
||||
stata-dark
|
||||
stata-light
|
||||
tango
|
||||
trac
|
||||
vim
|
||||
vs
|
||||
xcode
|
||||
zenburn"
|
||||
printf '%s\n' abap algol algol_nu arduino auto autumn borland bw colorful default emacs friendly fruity gruvbox-dark gruvbox-light igor inkpot lovelace manni material monokai murphy native paraiso-dark paraiso-light pastie perldoc pie pie-dark pie-light rainbow_dash rrt sas solarized solarized-dark solarized-light stata stata-dark stata-light tango trac vim vs xcode zenburn
|
||||
end
|
||||
|
||||
function __fish_httpie_mime_types
|
||||
test -r /usr/share/mime/types && cat /usr/share/mime/types
|
||||
end
|
||||
|
||||
function __fish_httpie_print_args
|
||||
set -l arg (commandline -t)
|
||||
string match -qe H "$arg" || echo -e $arg"H\trequest headers"
|
||||
string match -qe B "$arg" || echo -e $arg"B\trequest body"
|
||||
string match -qe h "$arg" || echo -e $arg"h\tresponse headers"
|
||||
string match -qe b "$arg" || echo -e $arg"b\tresponse body"
|
||||
string match -qe m "$arg" || echo -e $arg"m\tresponse metadata"
|
||||
end
|
||||
|
||||
function __fish_httpie_auth_types
|
||||
echo -e "basic\tBasic HTTP auth"
|
||||
echo -e "digest\tDigest HTTP auth"
|
||||
echo -e "bearer\tBearer HTTP Auth"
|
||||
end
|
||||
|
||||
function __fish_http_verify_options
|
||||
@ -54,6 +26,7 @@ function __fish_http_verify_options
|
||||
echo -e "no\tDisable cert verification"
|
||||
end
|
||||
|
||||
|
||||
# Predefined Content Types
|
||||
|
||||
complete -c http -s j -l json -d 'Data items are serialized as a JSON object'
|
||||
@ -74,17 +47,19 @@ complete -c http -l pretty -xa "all colors format none" -d 'Control
|
||||
complete -c http -s s -l style -xa "(__fish_httpie_styles)" -d 'Output coloring style'
|
||||
complete -c http -l unsorted -d 'Disables all sorting while formatting output'
|
||||
complete -c http -l sorted -d 'Re-enables all sorting options while formatting output'
|
||||
complete -c http -l response-charset -x -d 'Override the response encoding'
|
||||
complete -c http -l response-mime -xa "(__fish_httpie_mime_types)" -d 'Override the response mime type for coloring and formatting'
|
||||
complete -c http -l format-options -x -d 'Controls output formatting'
|
||||
|
||||
|
||||
# Output Options
|
||||
|
||||
complete -c http -s p -l print -x -d 'String specifying what the output should contain'
|
||||
complete -c http -s p -l print -xa "(__fish_httpie_print_args)" -d 'String specifying what the output should contain'
|
||||
complete -c http -s h -l headers -d 'Print only the response headers'
|
||||
complete -c http -s m -l meta -d 'Print only the response metadata'
|
||||
complete -c http -s b -l body -d 'Print only the response body'
|
||||
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
|
||||
complete -c http -l all -d 'Show any intermediary requests/responses'
|
||||
complete -c http -s P -l history-print -x -d 'The same as --print but applies only to intermediary requests/responses'
|
||||
complete -c http -s S -l stream -d 'Always stream the response body by line'
|
||||
complete -c http -s o -l output -F -d 'Save output to FILE'
|
||||
complete -c http -s d -l download -d 'Download a file'
|
||||
@ -115,7 +90,7 @@ complete -c http -l max-headers -x -d 'Maximum number of response headers
|
||||
complete -c http -l timeout -x -d 'Connection timeout in seconds'
|
||||
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
|
||||
complete -c http -l path-as-is -d 'Bypass dot segment URL squashing'
|
||||
complete -c http -l chunked -d ''
|
||||
complete -c http -l chunked -d 'Enable streaming via chunked transfer encoding'
|
||||
|
||||
|
||||
# SSL
|
||||
@ -125,12 +100,14 @@ complete -c http -l ssl -x -d 'Desired prot
|
||||
complete -c http -l ciphers -x -d 'String in the OpenSSL cipher list format'
|
||||
complete -c http -l cert -F -d 'Client side SSL certificate'
|
||||
complete -c http -l cert-key -F -d 'Private key to use with SSL'
|
||||
complete -c http -l cert-key-pass -x -d 'Passphrase for the given private key'
|
||||
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
complete -c http -s I -l ignore-stdin -d 'Do not attempt to read stdin'
|
||||
complete -c http -l help -d 'Show help'
|
||||
complete -c http -l manual -d 'Show the full manual'
|
||||
complete -c http -l version -d 'Show version'
|
||||
complete -c http -l traceback -d 'Prints exception traceback should one occur'
|
||||
complete -c http -l default-scheme -x -d 'The default scheme to use'
|
||||
|
@ -1,4 +1,5 @@
|
||||
.TH http 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||
.\" This file is auto-generated from the parser declaration in httpie/cli/definition.py by extras/scripts/generate_man_pages.py.
|
||||
.TH http 1 "2022-05-06" "HTTPie 3.2.1" "HTTPie Manual"
|
||||
.SH NAME
|
||||
http
|
||||
.SH SYNOPSIS
|
||||
@ -6,7 +7,7 @@ http [METHOD] URL [REQUEST_ITEM ...]
|
||||
|
||||
.SH DESCRIPTION
|
||||
HTTPie: modern, user-friendly command-line HTTP client for the API era. <https://httpie.io>
|
||||
.SH Positional Arguments
|
||||
.SH Positional arguments
|
||||
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
@ -27,8 +28,8 @@ is some data to be sent, otherwise GET:
|
||||
.IP "\fB\,URL\/\fR"
|
||||
|
||||
|
||||
The request URL. Scheme defaults to \'http://\' if the URL
|
||||
does not include one. (You can override this with:\fB\,--default-scheme\/\fR=http/https)
|
||||
The request URL. Scheme defaults to \[aq]http://\[aq] if the URL
|
||||
does not include one. (You can override this with: \fB\,--default-scheme\/\fR=http/https)
|
||||
|
||||
You can also use a shorthand for localhost
|
||||
|
||||
@ -43,44 +44,44 @@ You can also use a shorthand for localhost
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
\':\' HTTP headers:
|
||||
\[aq]:\[aq] HTTP headers:
|
||||
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
\'==\' URL parameters to be appended to the request URI:
|
||||
\[aq]==\[aq] URL parameters to be appended to the request URI:
|
||||
|
||||
search==httpie
|
||||
|
||||
\'=\' Data fields to be serialized into a JSON object (with\fB\,--json\/\fR,\fB\,-j\/\fR)
|
||||
or form data (with\fB\,--form\/\fR,\fB\,-f\/\fR):
|
||||
\[aq]=\[aq] Data fields to be serialized into a JSON object (with \fB\,--json\/\fR, \fB\,-j\/\fR)
|
||||
or form data (with \fB\,--form\/\fR, \fB\,-f\/\fR):
|
||||
|
||||
name=HTTPie language=Python description=\'CLI HTTP client\'
|
||||
name=HTTPie language=Python description=\[aq]CLI HTTP client\[aq]
|
||||
|
||||
\':=\' Non-string JSON data fields (only with\fB\,--json\/\fR,\fB\,-j\/\fR):
|
||||
\[aq]:=\[aq] Non-string JSON data fields (only with \fB\,--json\/\fR, \fB\,-j\/\fR):
|
||||
|
||||
awesome:=true amount:=42 colors:=\'["red", "green", "blue"]\'
|
||||
awesome:=true amount:=42 colors:=\[aq][\[dq]red\[dq], \[dq]green\[dq], \[dq]blue\[dq]]\[aq]
|
||||
|
||||
\'@\' Form file fields (only with\fB\,--form\/\fR or\fB\,--multipart\/\fR):
|
||||
\[aq]@\[aq] Form file fields (only with \fB\,--form\/\fR or \fB\,--multipart\/\fR):
|
||||
|
||||
cv@\~/Documents/CV.pdf
|
||||
cv@\'\~/Documents/CV.pdf;type=application/pdf\'
|
||||
cv@\(ti/Documents/CV.pdf
|
||||
cv@\[aq]\(ti/Documents/CV.pdf;type=application/pdf\[aq]
|
||||
|
||||
\'=@\' A data field like \'=\', but takes a file path and embeds its content:
|
||||
\[aq]=@\[aq] A data field like \[aq]=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
\':=@\' A raw JSON field like \':=\', but takes a file path and embeds its content:
|
||||
\[aq]:=@\[aq] A raw JSON field like \[aq]:=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
package:=@./package.json
|
||||
|
||||
You can use a backslash to escape a colliding separator in the field name:
|
||||
|
||||
field-name-with\\:colon=value
|
||||
field-name-with\e:colon=value
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Predefined Content Types
|
||||
.SH Predefined content types
|
||||
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||
|
||||
|
||||
@ -104,13 +105,13 @@ multipart/form-data request.
|
||||
.IP "\fB\,--multipart\/\fR"
|
||||
|
||||
|
||||
Similar to\fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||
Similar to \fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||
|
||||
|
||||
.IP "\fB\,--boundary\/\fR"
|
||||
|
||||
|
||||
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with\fB\,--form\/\fR.
|
||||
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with \fB\,--form\/\fR.
|
||||
|
||||
|
||||
.IP "\fB\,--raw\/\fR"
|
||||
@ -119,7 +120,7 @@ Specify a custom boundary string for multipart/form-data requests. Only has effe
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
|
||||
$ http\fB\,--raw\/\fR=\'data\' pie.dev/post
|
||||
$ http \fB\,--raw\/\fR=\[aq]data\[aq] pie.dev/post
|
||||
|
||||
You can achieve the same by piping the data via stdin:
|
||||
|
||||
@ -133,7 +134,7 @@ Or have HTTPie load the raw data from a file:
|
||||
|
||||
|
||||
.PP
|
||||
.SH Content Processing Options
|
||||
.SH Content processing options
|
||||
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||
|
||||
|
||||
@ -146,32 +147,32 @@ negative. Compression can be forced by repeating the argument.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output Processing
|
||||
.SH Output processing
|
||||
.IP "\fB\,--pretty\/\fR"
|
||||
|
||||
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
and formatting (default for terminal output), "colors", or "format".
|
||||
Controls output processing. The value can be \[dq]none\[dq] to not prettify
|
||||
the output (default for redirected output), \[dq]all\[dq] to apply both colors
|
||||
and formatting (default for terminal output), \[dq]colors\[dq], or \[dq]format\[dq].
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--style\/\fR, \fB\,-s\/\fR \fI\,STYLE\/\fR"
|
||||
|
||||
|
||||
Output coloring style (default is "auto"). It can be one of:
|
||||
Output coloring style (default is \[dq]auto\[dq]). It can be one of:
|
||||
|
||||
auto, pie, pie-dark, pie-light, solarized
|
||||
|
||||
|
||||
For finding out all available styles in your system, try:
|
||||
|
||||
$ http\fB\,--style\/\fR
|
||||
$ http \fB\,--style\/\fR
|
||||
|
||||
The "auto" style follows your terminal\'s ANSI color styles.
|
||||
The \[dq]auto\[dq] style follows your terminal\[aq]s ANSI color styles.
|
||||
For non-auto styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color\' in your \~/.bashrc).
|
||||
$TERM environment variable is set to \[dq]xterm-256color\[dq] or similar
|
||||
(e.g., via `export TERM=xterm-256color\[aq] in your \(ti/.bashrc).
|
||||
|
||||
.IP "\fB\,--unsorted\/\fR"
|
||||
|
||||
@ -215,7 +216,7 @@ Override the response mime type for coloring and formatting for the terminal, e.
|
||||
|
||||
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied)\fB\,--pretty\/\fR=all or\fB\,--pretty\/\fR=format.
|
||||
through (explicit or implied) \fB\,--pretty\/\fR=all or \fB\,--pretty\/\fR=format.
|
||||
The following are the default options:
|
||||
|
||||
headers.sort:true
|
||||
@ -236,19 +237,19 @@ This is something you will typically put into your config file.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output Options
|
||||
.SH Output options
|
||||
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||
|
||||
|
||||
String specifying what the output should contain:
|
||||
|
||||
\'H\' request headers
|
||||
\'B\' request body
|
||||
\'h\' response headers
|
||||
\'b\' response body
|
||||
\'m\' response metadata
|
||||
\[aq]H\[aq] request headers
|
||||
\[aq]B\[aq] request body
|
||||
\[aq]h\[aq] response headers
|
||||
\[aq]b\[aq] response body
|
||||
\[aq]m\[aq] response metadata
|
||||
|
||||
The default behaviour is \'hb\' (i.e., the response
|
||||
The default behaviour is \[aq]hb\[aq] (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
If the output is piped to another program or to a file, then only the
|
||||
response body is printed by default.
|
||||
@ -258,34 +259,34 @@ response body is printed by default.
|
||||
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||
|
||||
|
||||
Print only the response headers. Shortcut for\fB\,--print\/\fR=h.
|
||||
Print only the response headers. Shortcut for \fB\,--print\/\fR=h.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--meta\/\fR, \fB\,-m\/\fR"
|
||||
|
||||
|
||||
Print only the response metadata. Shortcut for\fB\,--print\/\fR=m.
|
||||
Print only the response metadata. Shortcut for \fB\,--print\/\fR=m.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||
|
||||
|
||||
Print only the response body. Shortcut for\fB\,--print\/\fR=b.
|
||||
Print only the response body. Shortcut for \fB\,--print\/\fR=b.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--verbose\/\fR, \fB\,-v\/\fR"
|
||||
|
||||
|
||||
Verbose output. For the level one (with single \fB\,-v\/\fR`/\fB\,--verbose\/\fR`), print
|
||||
Verbose output. For the level one (with single `\fB\,-v\/\fR`/`\fB\,--verbose\/\fR`), print
|
||||
the whole request as well as the response. Also print any intermediary
|
||||
requests/responses (such as redirects). For the second level and higher,
|
||||
print these as well as the response metadata.
|
||||
|
||||
Level one is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbh
|
||||
Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||
Level one is a shortcut for: \fB\,--all\/\fR \fB\,--print\/\fR=BHbh
|
||||
Level two is a shortcut for: \fB\,--all\/\fR \fB\,--print\/\fR=BHbhm
|
||||
|
||||
|
||||
.IP "\fB\,--all\/\fR"
|
||||
@ -293,23 +294,23 @@ Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
followed redirects (with\fB\,--follow\/\fR), the first unauthorized request when
|
||||
Digest auth is used \fB\,--auth\/\fR=digest), etc.
|
||||
followed redirects (with \fB\,--follow\/\fR), the first unauthorized request when
|
||||
Digest auth is used (\fB\,--auth\/\fR=digest), etc.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--stream\/\fR, \fB\,-S\/\fR"
|
||||
|
||||
|
||||
Always stream the response body by line, i.e., behave like `tail\fB\,-f\/\fR\'.
|
||||
Always stream the response body by line, i.e., behave like `tail \fB\,-f\/\fR\[aq].
|
||||
|
||||
Without\fB\,--stream\/\fR and with\fB\,--pretty\/\fR (either set or implied),
|
||||
Without \fB\,--stream\/\fR and with \fB\,--pretty\/\fR (either set or implied),
|
||||
HTTPie fetches the whole response before it outputs the processed data.
|
||||
|
||||
Set this option when you want to continuously display a prettified
|
||||
long-lived response, such as one from the Twitter streaming API.
|
||||
|
||||
It is useful also without\fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||
It is useful also without \fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||
more often and in smaller chunks.
|
||||
|
||||
|
||||
@ -317,7 +318,7 @@ more often and in smaller chunks.
|
||||
.IP "\fB\,--output\/\fR, \fB\,-o\/\fR \fI\,FILE\/\fR"
|
||||
|
||||
|
||||
Save output to FILE instead of stdout. If\fB\,--download\/\fR is also set, then only
|
||||
Save output to FILE instead of stdout. If \fB\,--download\/\fR is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
@ -327,7 +328,7 @@ printed to stderr.
|
||||
|
||||
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||
in a file. The filename is guessed unless specified with \fB\,--output\/\fR
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
|
||||
@ -335,7 +336,7 @@ in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||
|
||||
|
||||
Resume an interrupted download. Note that the\fB\,--output\/\fR option needs to be
|
||||
Resume an interrupted download. Note that the \fB\,--output\/\fR option needs to be
|
||||
specified as well.
|
||||
|
||||
|
||||
@ -345,8 +346,8 @@ specified as well.
|
||||
|
||||
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||
Provide twice to suppress warnings as well.
|
||||
stdout is still redirected if\fB\,--output\/\fR is specified.
|
||||
Flag doesn\'t affect behaviour of download beyond not printing to terminal.
|
||||
stdout is still redirected if \fB\,--output\/\fR is specified.
|
||||
Flag doesn\[aq]t affect behaviour of download beyond not printing to terminal.
|
||||
|
||||
|
||||
|
||||
@ -383,24 +384,24 @@ exchange.
|
||||
|
||||
For username/password based authentication mechanisms (e.g
|
||||
basic auth or digest auth) if only the username is provided
|
||||
\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||
(\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||
|
||||
|
||||
The authentication mechanism to be used. Defaults to "basic".
|
||||
The authentication mechanism to be used. Defaults to \[dq]basic\[dq].
|
||||
|
||||
"basic": Basic HTTP auth
|
||||
\[dq]basic\[dq]: Basic HTTP auth
|
||||
|
||||
"digest": Digest HTTP auth
|
||||
\[dq]digest\[dq]: Digest HTTP auth
|
||||
|
||||
"bearer": Bearer HTTP Auth
|
||||
\[dq]bearer\[dq]: Bearer HTTP Auth
|
||||
|
||||
For finding out all available authentication types in your system, try:
|
||||
|
||||
$ http\fB\,--auth-type\/\fR
|
||||
$ http \fB\,--auth-type\/\fR
|
||||
|
||||
.IP "\fB\,--ignore-netrc\/\fR"
|
||||
|
||||
@ -413,7 +414,7 @@ Ignore credentials from .netrc.
|
||||
.IP "\fB\,--offline\/\fR"
|
||||
|
||||
|
||||
Build the request and print it but don\'t actually send it.
|
||||
Build the request and print it but don\(gat actually send it.
|
||||
|
||||
|
||||
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||
@ -435,7 +436,7 @@ Follow 30x Location redirects.
|
||||
.IP "\fB\,--max-redirects\/\fR"
|
||||
|
||||
|
||||
By default, requests have a limit of 30 redirects (works with\fB\,--follow\/\fR).
|
||||
By default, requests have a limit of 30 redirects (works with \fB\,--follow\/\fR).
|
||||
|
||||
|
||||
|
||||
@ -466,7 +467,7 @@ exit with an error if the status indicates one.
|
||||
|
||||
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
|
||||
status code, HTTPie exits with 4 or 5 respectively. If the response is a
|
||||
3xx (Redirect) and\fB\,--follow\/\fR hasn\'t been set, then the exit status is 3.
|
||||
3xx (Redirect) and \fB\,--follow\/\fR hasn\[aq]t been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
|
||||
@ -488,8 +489,8 @@ Enable streaming via chunked transfer encoding. The Transfer-Encoding header is
|
||||
.IP "\fB\,--verify\/\fR"
|
||||
|
||||
|
||||
Set to "no" (or "false") to skip checking the host\'s SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
Set to \[dq]no\[dq] (or \[dq]false\[dq]) to skip checking the host\[aq]s SSL certificate.
|
||||
Defaults to \[dq]yes\[dq] (\[dq]true\[dq]). You can also pass the path to a CA_BUNDLE file
|
||||
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||
variable instead.)
|
||||
|
||||
@ -521,14 +522,14 @@ ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+A
|
||||
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
specify\fB\,--cert-key\/\fR separately.
|
||||
specify \fB\,--cert-key\/\fR separately.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key\/\fR"
|
||||
|
||||
|
||||
The private key to use with SSL. Only needed if\fB\,--cert\/\fR is given and the
|
||||
The private key to use with SSL. Only needed if \fB\,--cert\/\fR is given and the
|
||||
certificate file does not contain the private key.
|
||||
|
||||
|
||||
@ -536,9 +537,9 @@ certificate file does not contain the private key.
|
||||
.IP "\fB\,--cert-key-pass\/\fR"
|
||||
|
||||
|
||||
The passphrase to be used to with the given private key. Only needed if\fB\,--cert-key\/\fR
|
||||
The passphrase to be used to with the given private key. Only needed if \fB\,--cert-key\/\fR
|
||||
is given and the key file requires a passphrase.
|
||||
If not provided, you\'ll be prompted interactively.
|
||||
If not provided, you\(gall be prompted interactively.
|
||||
|
||||
|
||||
.PP
|
||||
@ -588,3 +589,10 @@ information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
|
||||
For every \fB\,--OPTION\/\fR there is also a \fB\,--no-OPTION\/\fR that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
https://github.com/httpie/httpie/issues
|
@ -1,9 +1,9 @@
|
||||
.TH httpie 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||
.\" This file is auto-generated from the parser declaration in httpie/manager/cli.py by extras/scripts/generate_man_pages.py.
|
||||
.TH httpie 1 "2022-05-06" "HTTPie 3.2.1" "HTTPie Manual"
|
||||
.SH NAME
|
||||
httpie
|
||||
.SH SYNOPSIS
|
||||
httpie HOSTNAME SESSION_NAME_OR_PATH TARGET TARGET TARGET TARGET TARGET TARGET
|
||||
|
||||
httpie
|
||||
.SH DESCRIPTION
|
||||
|
||||
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
|
||||
@ -12,12 +12,21 @@ Be aware that you might be looking for http/https commands for sending
|
||||
HTTP requests. This command is only available for managing the HTTTPie
|
||||
plugins and the configuration around it.
|
||||
|
||||
|
||||
If you are looking for the man pages of http/https commands, try one of the following:
|
||||
$ man http
|
||||
$ man https
|
||||
|
||||
|
||||
.SH httpie cli export-args
|
||||
Export available options for the CLI
|
||||
.IP "\fB\,-f\/\fR, \fB\,--format\/\fR"
|
||||
|
||||
Format to export in.
|
||||
|
||||
|
||||
.PP
|
||||
.SH httpie cli check-updates
|
||||
Check for updates
|
||||
.PP
|
||||
.SH httpie cli sessions upgrade
|
||||
Upgrade the given HTTPie session with the latest layout. A list of changes between different session versions can be found in the official documentation.
|
||||
|
@ -1,4 +1,5 @@
|
||||
.TH https 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||
.\" This file is auto-generated from the parser declaration in httpie/cli/definition.py by extras/scripts/generate_man_pages.py.
|
||||
.TH https 1 "2022-05-06" "HTTPie 3.2.1" "HTTPie Manual"
|
||||
.SH NAME
|
||||
https
|
||||
.SH SYNOPSIS
|
||||
@ -6,7 +7,7 @@ https [METHOD] URL [REQUEST_ITEM ...]
|
||||
|
||||
.SH DESCRIPTION
|
||||
HTTPie: modern, user-friendly command-line HTTP client for the API era. <https://httpie.io>
|
||||
.SH Positional Arguments
|
||||
.SH Positional arguments
|
||||
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
@ -27,8 +28,8 @@ is some data to be sent, otherwise GET:
|
||||
.IP "\fB\,URL\/\fR"
|
||||
|
||||
|
||||
The request URL. Scheme defaults to \'http://\' if the URL
|
||||
does not include one. (You can override this with:\fB\,--default-scheme\/\fR=http/https)
|
||||
The request URL. Scheme defaults to \[aq]http://\[aq] if the URL
|
||||
does not include one. (You can override this with: \fB\,--default-scheme\/\fR=http/https)
|
||||
|
||||
You can also use a shorthand for localhost
|
||||
|
||||
@ -43,44 +44,44 @@ You can also use a shorthand for localhost
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
\':\' HTTP headers:
|
||||
\[aq]:\[aq] HTTP headers:
|
||||
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
\'==\' URL parameters to be appended to the request URI:
|
||||
\[aq]==\[aq] URL parameters to be appended to the request URI:
|
||||
|
||||
search==httpie
|
||||
|
||||
\'=\' Data fields to be serialized into a JSON object (with\fB\,--json\/\fR,\fB\,-j\/\fR)
|
||||
or form data (with\fB\,--form\/\fR,\fB\,-f\/\fR):
|
||||
\[aq]=\[aq] Data fields to be serialized into a JSON object (with \fB\,--json\/\fR, \fB\,-j\/\fR)
|
||||
or form data (with \fB\,--form\/\fR, \fB\,-f\/\fR):
|
||||
|
||||
name=HTTPie language=Python description=\'CLI HTTP client\'
|
||||
name=HTTPie language=Python description=\[aq]CLI HTTP client\[aq]
|
||||
|
||||
\':=\' Non-string JSON data fields (only with\fB\,--json\/\fR,\fB\,-j\/\fR):
|
||||
\[aq]:=\[aq] Non-string JSON data fields (only with \fB\,--json\/\fR, \fB\,-j\/\fR):
|
||||
|
||||
awesome:=true amount:=42 colors:=\'["red", "green", "blue"]\'
|
||||
awesome:=true amount:=42 colors:=\[aq][\[dq]red\[dq], \[dq]green\[dq], \[dq]blue\[dq]]\[aq]
|
||||
|
||||
\'@\' Form file fields (only with\fB\,--form\/\fR or\fB\,--multipart\/\fR):
|
||||
\[aq]@\[aq] Form file fields (only with \fB\,--form\/\fR or \fB\,--multipart\/\fR):
|
||||
|
||||
cv@\~/Documents/CV.pdf
|
||||
cv@\'\~/Documents/CV.pdf;type=application/pdf\'
|
||||
cv@\(ti/Documents/CV.pdf
|
||||
cv@\[aq]\(ti/Documents/CV.pdf;type=application/pdf\[aq]
|
||||
|
||||
\'=@\' A data field like \'=\', but takes a file path and embeds its content:
|
||||
\[aq]=@\[aq] A data field like \[aq]=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
\':=@\' A raw JSON field like \':=\', but takes a file path and embeds its content:
|
||||
\[aq]:=@\[aq] A raw JSON field like \[aq]:=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
package:=@./package.json
|
||||
|
||||
You can use a backslash to escape a colliding separator in the field name:
|
||||
|
||||
field-name-with\\:colon=value
|
||||
field-name-with\e:colon=value
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Predefined Content Types
|
||||
.SH Predefined content types
|
||||
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||
|
||||
|
||||
@ -104,13 +105,13 @@ multipart/form-data request.
|
||||
.IP "\fB\,--multipart\/\fR"
|
||||
|
||||
|
||||
Similar to\fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||
Similar to \fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||
|
||||
|
||||
.IP "\fB\,--boundary\/\fR"
|
||||
|
||||
|
||||
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with\fB\,--form\/\fR.
|
||||
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with \fB\,--form\/\fR.
|
||||
|
||||
|
||||
.IP "\fB\,--raw\/\fR"
|
||||
@ -119,7 +120,7 @@ Specify a custom boundary string for multipart/form-data requests. Only has effe
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
|
||||
$ http\fB\,--raw\/\fR=\'data\' pie.dev/post
|
||||
$ http \fB\,--raw\/\fR=\[aq]data\[aq] pie.dev/post
|
||||
|
||||
You can achieve the same by piping the data via stdin:
|
||||
|
||||
@ -133,7 +134,7 @@ Or have HTTPie load the raw data from a file:
|
||||
|
||||
|
||||
.PP
|
||||
.SH Content Processing Options
|
||||
.SH Content processing options
|
||||
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||
|
||||
|
||||
@ -146,32 +147,32 @@ negative. Compression can be forced by repeating the argument.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output Processing
|
||||
.SH Output processing
|
||||
.IP "\fB\,--pretty\/\fR"
|
||||
|
||||
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
and formatting (default for terminal output), "colors", or "format".
|
||||
Controls output processing. The value can be \[dq]none\[dq] to not prettify
|
||||
the output (default for redirected output), \[dq]all\[dq] to apply both colors
|
||||
and formatting (default for terminal output), \[dq]colors\[dq], or \[dq]format\[dq].
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--style\/\fR, \fB\,-s\/\fR \fI\,STYLE\/\fR"
|
||||
|
||||
|
||||
Output coloring style (default is "auto"). It can be one of:
|
||||
Output coloring style (default is \[dq]auto\[dq]). It can be one of:
|
||||
|
||||
auto, pie, pie-dark, pie-light, solarized
|
||||
|
||||
|
||||
For finding out all available styles in your system, try:
|
||||
|
||||
$ http\fB\,--style\/\fR
|
||||
$ http \fB\,--style\/\fR
|
||||
|
||||
The "auto" style follows your terminal\'s ANSI color styles.
|
||||
The \[dq]auto\[dq] style follows your terminal\[aq]s ANSI color styles.
|
||||
For non-auto styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color\' in your \~/.bashrc).
|
||||
$TERM environment variable is set to \[dq]xterm-256color\[dq] or similar
|
||||
(e.g., via `export TERM=xterm-256color\[aq] in your \(ti/.bashrc).
|
||||
|
||||
.IP "\fB\,--unsorted\/\fR"
|
||||
|
||||
@ -215,7 +216,7 @@ Override the response mime type for coloring and formatting for the terminal, e.
|
||||
|
||||
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied)\fB\,--pretty\/\fR=all or\fB\,--pretty\/\fR=format.
|
||||
through (explicit or implied) \fB\,--pretty\/\fR=all or \fB\,--pretty\/\fR=format.
|
||||
The following are the default options:
|
||||
|
||||
headers.sort:true
|
||||
@ -236,19 +237,19 @@ This is something you will typically put into your config file.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output Options
|
||||
.SH Output options
|
||||
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||
|
||||
|
||||
String specifying what the output should contain:
|
||||
|
||||
\'H\' request headers
|
||||
\'B\' request body
|
||||
\'h\' response headers
|
||||
\'b\' response body
|
||||
\'m\' response metadata
|
||||
\[aq]H\[aq] request headers
|
||||
\[aq]B\[aq] request body
|
||||
\[aq]h\[aq] response headers
|
||||
\[aq]b\[aq] response body
|
||||
\[aq]m\[aq] response metadata
|
||||
|
||||
The default behaviour is \'hb\' (i.e., the response
|
||||
The default behaviour is \[aq]hb\[aq] (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
If the output is piped to another program or to a file, then only the
|
||||
response body is printed by default.
|
||||
@ -258,34 +259,34 @@ response body is printed by default.
|
||||
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||
|
||||
|
||||
Print only the response headers. Shortcut for\fB\,--print\/\fR=h.
|
||||
Print only the response headers. Shortcut for \fB\,--print\/\fR=h.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--meta\/\fR, \fB\,-m\/\fR"
|
||||
|
||||
|
||||
Print only the response metadata. Shortcut for\fB\,--print\/\fR=m.
|
||||
Print only the response metadata. Shortcut for \fB\,--print\/\fR=m.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||
|
||||
|
||||
Print only the response body. Shortcut for\fB\,--print\/\fR=b.
|
||||
Print only the response body. Shortcut for \fB\,--print\/\fR=b.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--verbose\/\fR, \fB\,-v\/\fR"
|
||||
|
||||
|
||||
Verbose output. For the level one (with single \fB\,-v\/\fR`/\fB\,--verbose\/\fR`), print
|
||||
Verbose output. For the level one (with single `\fB\,-v\/\fR`/`\fB\,--verbose\/\fR`), print
|
||||
the whole request as well as the response. Also print any intermediary
|
||||
requests/responses (such as redirects). For the second level and higher,
|
||||
print these as well as the response metadata.
|
||||
|
||||
Level one is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbh
|
||||
Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||
Level one is a shortcut for: \fB\,--all\/\fR \fB\,--print\/\fR=BHbh
|
||||
Level two is a shortcut for: \fB\,--all\/\fR \fB\,--print\/\fR=BHbhm
|
||||
|
||||
|
||||
.IP "\fB\,--all\/\fR"
|
||||
@ -293,23 +294,23 @@ Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
followed redirects (with\fB\,--follow\/\fR), the first unauthorized request when
|
||||
Digest auth is used \fB\,--auth\/\fR=digest), etc.
|
||||
followed redirects (with \fB\,--follow\/\fR), the first unauthorized request when
|
||||
Digest auth is used (\fB\,--auth\/\fR=digest), etc.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--stream\/\fR, \fB\,-S\/\fR"
|
||||
|
||||
|
||||
Always stream the response body by line, i.e., behave like `tail\fB\,-f\/\fR\'.
|
||||
Always stream the response body by line, i.e., behave like `tail \fB\,-f\/\fR\[aq].
|
||||
|
||||
Without\fB\,--stream\/\fR and with\fB\,--pretty\/\fR (either set or implied),
|
||||
Without \fB\,--stream\/\fR and with \fB\,--pretty\/\fR (either set or implied),
|
||||
HTTPie fetches the whole response before it outputs the processed data.
|
||||
|
||||
Set this option when you want to continuously display a prettified
|
||||
long-lived response, such as one from the Twitter streaming API.
|
||||
|
||||
It is useful also without\fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||
It is useful also without \fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||
more often and in smaller chunks.
|
||||
|
||||
|
||||
@ -317,7 +318,7 @@ more often and in smaller chunks.
|
||||
.IP "\fB\,--output\/\fR, \fB\,-o\/\fR \fI\,FILE\/\fR"
|
||||
|
||||
|
||||
Save output to FILE instead of stdout. If\fB\,--download\/\fR is also set, then only
|
||||
Save output to FILE instead of stdout. If \fB\,--download\/\fR is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
@ -327,7 +328,7 @@ printed to stderr.
|
||||
|
||||
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||
in a file. The filename is guessed unless specified with \fB\,--output\/\fR
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
|
||||
@ -335,7 +336,7 @@ in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||
|
||||
|
||||
Resume an interrupted download. Note that the\fB\,--output\/\fR option needs to be
|
||||
Resume an interrupted download. Note that the \fB\,--output\/\fR option needs to be
|
||||
specified as well.
|
||||
|
||||
|
||||
@ -345,8 +346,8 @@ specified as well.
|
||||
|
||||
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||
Provide twice to suppress warnings as well.
|
||||
stdout is still redirected if\fB\,--output\/\fR is specified.
|
||||
Flag doesn\'t affect behaviour of download beyond not printing to terminal.
|
||||
stdout is still redirected if \fB\,--output\/\fR is specified.
|
||||
Flag doesn\[aq]t affect behaviour of download beyond not printing to terminal.
|
||||
|
||||
|
||||
|
||||
@ -383,24 +384,24 @@ exchange.
|
||||
|
||||
For username/password based authentication mechanisms (e.g
|
||||
basic auth or digest auth) if only the username is provided
|
||||
\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||
(\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||
|
||||
|
||||
The authentication mechanism to be used. Defaults to "basic".
|
||||
The authentication mechanism to be used. Defaults to \[dq]basic\[dq].
|
||||
|
||||
"basic": Basic HTTP auth
|
||||
\[dq]basic\[dq]: Basic HTTP auth
|
||||
|
||||
"digest": Digest HTTP auth
|
||||
\[dq]digest\[dq]: Digest HTTP auth
|
||||
|
||||
"bearer": Bearer HTTP Auth
|
||||
\[dq]bearer\[dq]: Bearer HTTP Auth
|
||||
|
||||
For finding out all available authentication types in your system, try:
|
||||
|
||||
$ http\fB\,--auth-type\/\fR
|
||||
$ http \fB\,--auth-type\/\fR
|
||||
|
||||
.IP "\fB\,--ignore-netrc\/\fR"
|
||||
|
||||
@ -413,7 +414,7 @@ Ignore credentials from .netrc.
|
||||
.IP "\fB\,--offline\/\fR"
|
||||
|
||||
|
||||
Build the request and print it but don\'t actually send it.
|
||||
Build the request and print it but don\(gat actually send it.
|
||||
|
||||
|
||||
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||
@ -435,7 +436,7 @@ Follow 30x Location redirects.
|
||||
.IP "\fB\,--max-redirects\/\fR"
|
||||
|
||||
|
||||
By default, requests have a limit of 30 redirects (works with\fB\,--follow\/\fR).
|
||||
By default, requests have a limit of 30 redirects (works with \fB\,--follow\/\fR).
|
||||
|
||||
|
||||
|
||||
@ -466,7 +467,7 @@ exit with an error if the status indicates one.
|
||||
|
||||
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
|
||||
status code, HTTPie exits with 4 or 5 respectively. If the response is a
|
||||
3xx (Redirect) and\fB\,--follow\/\fR hasn\'t been set, then the exit status is 3.
|
||||
3xx (Redirect) and \fB\,--follow\/\fR hasn\[aq]t been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
|
||||
@ -488,8 +489,8 @@ Enable streaming via chunked transfer encoding. The Transfer-Encoding header is
|
||||
.IP "\fB\,--verify\/\fR"
|
||||
|
||||
|
||||
Set to "no" (or "false") to skip checking the host\'s SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
Set to \[dq]no\[dq] (or \[dq]false\[dq]) to skip checking the host\[aq]s SSL certificate.
|
||||
Defaults to \[dq]yes\[dq] (\[dq]true\[dq]). You can also pass the path to a CA_BUNDLE file
|
||||
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||
variable instead.)
|
||||
|
||||
@ -521,14 +522,14 @@ ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+A
|
||||
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
specify\fB\,--cert-key\/\fR separately.
|
||||
specify \fB\,--cert-key\/\fR separately.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key\/\fR"
|
||||
|
||||
|
||||
The private key to use with SSL. Only needed if\fB\,--cert\/\fR is given and the
|
||||
The private key to use with SSL. Only needed if \fB\,--cert\/\fR is given and the
|
||||
certificate file does not contain the private key.
|
||||
|
||||
|
||||
@ -536,9 +537,9 @@ certificate file does not contain the private key.
|
||||
.IP "\fB\,--cert-key-pass\/\fR"
|
||||
|
||||
|
||||
The passphrase to be used to with the given private key. Only needed if\fB\,--cert-key\/\fR
|
||||
The passphrase to be used to with the given private key. Only needed if \fB\,--cert-key\/\fR
|
||||
is given and the key file requires a passphrase.
|
||||
If not provided, you\'ll be prompted interactively.
|
||||
If not provided, you\(gall be prompted interactively.
|
||||
|
||||
|
||||
.PP
|
||||
@ -588,3 +589,10 @@ information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
|
||||
.PP
|
||||
.SH SEE ALSO
|
||||
|
||||
For every \fB\,--OPTION\/\fR there is also a \fB\,--no-OPTION\/\fR that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
https://github.com/httpie/httpie/issues
|
@ -27,6 +27,7 @@ RUN python -m pip install /app
|
||||
RUN python -m pip install pyinstaller wheel
|
||||
RUN python -m pip install --force-reinstall --upgrade pip
|
||||
|
||||
RUN echo 'BUILD_CHANNEL="pypi"' > /app/httpie/internal/__build_channel__.py
|
||||
RUN python build.py
|
||||
|
||||
ENTRYPOINT ["mv", "/app/extras/packaging/linux/dist/", "/artifacts"]
|
||||
|
@ -6,6 +6,9 @@ from typing import Iterator, Tuple
|
||||
BUILD_DIR = Path(__file__).parent
|
||||
HTTPIE_DIR = BUILD_DIR.parent.parent.parent
|
||||
|
||||
EXTRAS_DIR = HTTPIE_DIR / 'extras'
|
||||
MAN_PAGES_DIR = EXTRAS_DIR / 'man'
|
||||
|
||||
SCRIPT_DIR = BUILD_DIR / Path('scripts')
|
||||
HOOKS_DIR = SCRIPT_DIR / 'hooks'
|
||||
|
||||
@ -50,6 +53,11 @@ def build_packages(http_binary: Path, httpie_binary: Path) -> None:
|
||||
(http_binary, '/usr/bin/https'),
|
||||
(httpie_binary, '/usr/bin/httpie'),
|
||||
]
|
||||
files.extend(
|
||||
(man_page, f'/usr/share/man/man1/{man_page.name}')
|
||||
for man_page in MAN_PAGES_DIR.glob('*.1')
|
||||
)
|
||||
|
||||
# A list of additional dependencies
|
||||
deps = [
|
||||
'python3 >= 3.7',
|
||||
@ -92,8 +100,9 @@ def main():
|
||||
build_packages(binaries['http_cli'], binaries['httpie_cli'])
|
||||
|
||||
# Rename http_cli/httpie_cli to http/httpie
|
||||
binaries['http_cli'].rename('http')
|
||||
binaries['httpie_cli'].rename('httpie')
|
||||
binaries['http_cli'].rename(DIST_DIR / 'http')
|
||||
binaries['httpie_cli'].rename(DIST_DIR / 'httpie')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -10,7 +10,7 @@ Ensure the following requirements are satisfied:
|
||||
- Python 3.7+
|
||||
- `pyperf`
|
||||
|
||||
Then, run the `extras/benchmarks/run.py`:
|
||||
Then, run the `extras/profiling/run.py`:
|
||||
|
||||
```console
|
||||
$ python extras/profiling/run.py
|
||||
|
@ -9,11 +9,11 @@ timings.
|
||||
|
||||
The benchmarks are run through 'pyperf', which allows to
|
||||
do get very precise results. For micro-benchmarks like startup,
|
||||
please run `pyperf system tune` to get even more acurrate results.
|
||||
please run `pyperf system tune` to get even more accurate results.
|
||||
|
||||
Examples:
|
||||
|
||||
# Run everything as usual, the default is that we do 3 warmup runs
|
||||
# Run everything as usual, the default is that we do 3 warm-up runs
|
||||
# and 5 actual runs.
|
||||
$ python extras/profiling/benchmarks.py
|
||||
|
||||
@ -188,7 +188,7 @@ DownloadRunner('download', '`http --download :/big_file.txt` (3GB)', '3G')
|
||||
def main() -> None:
|
||||
# PyPerf will bring it's own argument parser, so configure the script.
|
||||
# The somewhat fast and also precise enough configuration is this. We run
|
||||
# benchmarks 3 times to warmup (e.g especially for download benchmark, this
|
||||
# benchmarks 3 times to warm up (e.g especially for download benchmark, this
|
||||
# is important). And then 5 actual runs where we record.
|
||||
sys.argv.extend(
|
||||
['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2']
|
||||
|
@ -19,19 +19,19 @@ which would include additional dependencies like pyOpenSSL.
|
||||
Examples:
|
||||
|
||||
# Run everything as usual, and compare last commit with master
|
||||
$ python extras/benchmarks/run.py
|
||||
$ python extras/profiling/run.py
|
||||
|
||||
# Include complex environments
|
||||
$ python extras/benchmarks/run.py --complex
|
||||
$ python extras/profiling/run.py --complex
|
||||
|
||||
# Compare against a fresh copy
|
||||
$ python extras/benchmarks/run.py --fresh
|
||||
$ python extras/profiling/run.py --fresh
|
||||
|
||||
# Compare against a custom branch of a custom repo
|
||||
$ python extras/benchmarks/run.py --target-repo my_repo --target-branch my_branch
|
||||
$ python extras/profiling/run.py --target-repo my_repo --target-branch my_branch
|
||||
|
||||
# Debug changes made on this script (only run benchmarks once)
|
||||
$ python extras/benchmarks/run.py --debug
|
||||
$ python extras/profiling/run.py --debug
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
|
@ -9,21 +9,24 @@ from httpie.cli.options import ParserSpec
|
||||
from httpie.manager.cli import options as manager_options
|
||||
from httpie.output.ui.rich_help import OptionsHighlighter, to_usage
|
||||
from httpie.output.ui.rich_utils import render_as_string
|
||||
from httpie.utils import split
|
||||
from httpie.utils import split_iterable
|
||||
|
||||
|
||||
# Escape certain characters so they are rendered properly on
|
||||
# all terminals.
|
||||
# https://man7.org/linux/man-pages/man7/groff_char.7.html
|
||||
ESCAPE_MAP = {
|
||||
"'": "\\'",
|
||||
'~': '\\~',
|
||||
'’': "\\'",
|
||||
'\\': '\\\\',
|
||||
'"': '\[dq]',
|
||||
"'": '\[aq]',
|
||||
'~': '\(ti',
|
||||
'’': "\(ga",
|
||||
'\\': '\e',
|
||||
}
|
||||
ESCAPE_MAP = {ord(key): value for key, value in ESCAPE_MAP.items()}
|
||||
|
||||
EXTRAS_DIR = Path(__file__).parent.parent
|
||||
MAN_PAGE_PATH = EXTRAS_DIR / 'man'
|
||||
PROJECT_ROOT = EXTRAS_DIR.parent
|
||||
|
||||
OPTION_HIGHLIGHT_RE = re.compile(
|
||||
OptionsHighlighter.highlights[0]
|
||||
@ -57,6 +60,18 @@ class ManPageBuilder:
|
||||
def separate(self) -> None:
|
||||
self.source.append('.PP')
|
||||
|
||||
def format_desc(self, desc: str) -> str:
|
||||
description = _escape_and_dedent(desc)
|
||||
description = OPTION_HIGHLIGHT_RE.sub(
|
||||
# Boldify the option part, but don't remove the prefix (start of the match).
|
||||
lambda match: match[1] + self.boldify(match['option']),
|
||||
description
|
||||
)
|
||||
return description
|
||||
|
||||
def add_comment(self, comment: str) -> None:
|
||||
self.source.append(f'.\\" {comment}')
|
||||
|
||||
def add_options(self, options: Iterable[str], *, metavar: Optional[str] = None) -> None:
|
||||
text = ", ".join(map(self.boldify, options))
|
||||
if metavar:
|
||||
@ -92,8 +107,13 @@ def _escape_and_dedent(text: str) -> str:
|
||||
return '\n'.join(lines).translate(ESCAPE_MAP)
|
||||
|
||||
|
||||
def to_man_page(program_name: str, spec: ParserSpec) -> str:
|
||||
def to_man_page(program_name: str, spec: ParserSpec, *, is_top_level_cmd: bool = False) -> str:
|
||||
builder = ManPageBuilder()
|
||||
builder.add_comment(
|
||||
f"This file is auto-generated from the parser declaration "
|
||||
+ (f"in {Path(spec.source_file).relative_to(PROJECT_ROOT)} " if spec.source_file else "")
|
||||
+ f"by {Path(__file__).relative_to(PROJECT_ROOT)}."
|
||||
)
|
||||
|
||||
builder.title_line(
|
||||
full_name='HTTPie',
|
||||
@ -104,10 +124,19 @@ def to_man_page(program_name: str, spec: ParserSpec) -> str:
|
||||
builder.set_name(program_name)
|
||||
|
||||
with builder.section('SYNOPSIS'):
|
||||
builder.write(render_as_string(to_usage(spec, program_name=program_name)))
|
||||
# `http` and `https` are commands that can be directly used, so they can have
|
||||
# have a valid usage. But `httpie` is a top-level command with multiple sub commands,
|
||||
# so for the synopsis we'll only reference the `httpie` name.
|
||||
if is_top_level_cmd:
|
||||
synopsis = program_name
|
||||
else:
|
||||
synopsis = render_as_string(to_usage(spec, program_name=program_name))
|
||||
builder.write(synopsis)
|
||||
|
||||
with builder.section('DESCRIPTION'):
|
||||
builder.write(spec.description)
|
||||
if spec.man_page_hint:
|
||||
builder.write(spec.man_page_hint)
|
||||
|
||||
for index, group in enumerate(spec.groups, 1):
|
||||
with builder.section(group.name):
|
||||
@ -127,28 +156,26 @@ def to_man_page(program_name: str, spec: ParserSpec) -> str:
|
||||
metavar = None
|
||||
builder.add_options(raw_arg['options'], metavar=metavar)
|
||||
|
||||
description = _escape_and_dedent(raw_arg.get('description', ''))
|
||||
description = OPTION_HIGHLIGHT_RE.sub(
|
||||
lambda match: builder.boldify(match['option']),
|
||||
description
|
||||
)
|
||||
builder.write('\n' + description + '\n')
|
||||
desc = builder.format_desc(raw_arg.get('description', ''))
|
||||
builder.write('\n' + desc + '\n')
|
||||
|
||||
builder.separate()
|
||||
|
||||
|
||||
if spec.epilog:
|
||||
with builder.section('SEE ALSO'):
|
||||
builder.write(builder.format_desc(spec.epilog))
|
||||
|
||||
return builder.build()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for program_name, spec in [
|
||||
('http', core_options),
|
||||
('https', core_options),
|
||||
('httpie', manager_options),
|
||||
for program_name, spec, config in [
|
||||
('http', core_options, {}),
|
||||
('https', core_options, {}),
|
||||
('httpie', manager_options, {'is_top_level_cmd': True}),
|
||||
]:
|
||||
with open((MAN_PAGE_PATH / program_name).with_suffix('.1'), 'w') as stream:
|
||||
stream.write(to_man_page(program_name, spec))
|
||||
stream.write(to_man_page(program_name, spec, **config))
|
||||
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ HTTPie: modern, user-friendly command-line HTTP client for the API era.
|
||||
|
||||
"""
|
||||
|
||||
__version__ = '3.1.1.dev0'
|
||||
__date__ = '2022-03-08'
|
||||
__version__ = '3.2.2'
|
||||
__date__ = '2022-05-06'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -572,12 +572,6 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
highlight=False
|
||||
)
|
||||
|
||||
def print_help(self):
|
||||
from httpie.output.ui import rich_help
|
||||
|
||||
for renderable in rich_help.to_help_message(self.spec):
|
||||
self.env.rich_console.print(renderable)
|
||||
|
||||
def print_usage(self, file):
|
||||
from rich.text import Text
|
||||
from httpie.output.ui import rich_help
|
||||
|
@ -132,10 +132,3 @@ class RequestType(enum.Enum):
|
||||
FORM = enum.auto()
|
||||
MULTIPART = enum.auto()
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
EMPTY_STRING = ''
|
||||
OPEN_BRACKET = '['
|
||||
CLOSE_BRACKET = ']'
|
||||
BACKSLASH = '\\'
|
||||
HIGHLIGHTER = '^'
|
||||
|
@ -20,22 +20,19 @@ from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE, BUNDLED_
|
||||
get_available_styles)
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_STRING
|
||||
|
||||
options = ParserSpec(
|
||||
'http',
|
||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||
epilog="""
|
||||
To learn more, you can try:
|
||||
-> running 'http --manual'
|
||||
-> visiting our full documentation at https://httpie.io/docs/cli
|
||||
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
https://github.com/httpie/httpie/issues
|
||||
""",
|
||||
source_file=__file__
|
||||
)
|
||||
|
||||
|
||||
@ -835,9 +832,9 @@ ssl.add_argument(
|
||||
help=f"""
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
is used:
|
||||
ciphers are used on your system:
|
||||
|
||||
{DEFAULT_SSL_CIPHERS}
|
||||
{DEFAULT_SSL_CIPHERS_STRING}
|
||||
|
||||
""",
|
||||
)
|
||||
|
@ -92,7 +92,3 @@ class MultipartRequestDataDict(MultiValueOrderedDict):
|
||||
|
||||
class RequestFilesDict(RequestDataDict):
|
||||
pass
|
||||
|
||||
|
||||
class NestedJSONArray(list):
|
||||
"""Denotes a top-level JSON array."""
|
||||
|
@ -1,392 +0,0 @@
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Any,
|
||||
Iterator,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
List,
|
||||
NoReturn,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from httpie.cli.dicts import NestedJSONArray
|
||||
from httpie.cli.constants import EMPTY_STRING, OPEN_BRACKET, CLOSE_BRACKET, BACKSLASH, HIGHLIGHTER
|
||||
|
||||
|
||||
class HTTPieSyntaxError(ValueError):
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
token: Optional['Token'],
|
||||
message: str,
|
||||
message_kind: str = 'Syntax',
|
||||
) -> None:
|
||||
self.source = source
|
||||
self.token = token
|
||||
self.message = message
|
||||
self.message_kind = message_kind
|
||||
|
||||
def __str__(self):
|
||||
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
|
||||
if self.token is not None:
|
||||
lines.append(self.source)
|
||||
lines.append(
|
||||
' ' * (self.token.start)
|
||||
+ HIGHLIGHTER * (self.token.end - self.token.start)
|
||||
)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class TokenKind(Enum):
|
||||
TEXT = auto()
|
||||
NUMBER = auto()
|
||||
LEFT_BRACKET = auto()
|
||||
RIGHT_BRACKET = auto()
|
||||
|
||||
def to_name(self) -> str:
|
||||
for key, value in OPERATORS.items():
|
||||
if value is self:
|
||||
return repr(key)
|
||||
else:
|
||||
return 'a ' + self.name.lower()
|
||||
|
||||
|
||||
OPERATORS = {OPEN_BRACKET: TokenKind.LEFT_BRACKET, CLOSE_BRACKET: TokenKind.RIGHT_BRACKET}
|
||||
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
|
||||
LITERAL_TOKENS = [TokenKind.TEXT, TokenKind.NUMBER]
|
||||
|
||||
|
||||
class Token(NamedTuple):
|
||||
kind: TokenKind
|
||||
value: Union[str, int]
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
def assert_cant_happen() -> NoReturn:
|
||||
raise ValueError('Unexpected value')
|
||||
|
||||
|
||||
def check_escaped_int(value: str) -> str:
|
||||
if not value.startswith(BACKSLASH):
|
||||
raise ValueError('Not an escaped int')
|
||||
|
||||
try:
|
||||
int(value[1:])
|
||||
except ValueError as exc:
|
||||
raise ValueError('Not an escaped int') from exc
|
||||
else:
|
||||
return value[1:]
|
||||
|
||||
|
||||
def tokenize(source: str) -> Iterator[Token]:
|
||||
cursor = 0
|
||||
backslashes = 0
|
||||
buffer = []
|
||||
|
||||
def send_buffer() -> Iterator[Token]:
|
||||
nonlocal backslashes
|
||||
if not buffer:
|
||||
return None
|
||||
|
||||
value = ''.join(buffer)
|
||||
kind = TokenKind.TEXT
|
||||
if not backslashes:
|
||||
for variation, kind in [
|
||||
(int, TokenKind.NUMBER),
|
||||
(check_escaped_int, TokenKind.TEXT),
|
||||
]:
|
||||
try:
|
||||
value = variation(value)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
yield Token(
|
||||
kind, value, start=cursor - (len(buffer) + backslashes), end=cursor
|
||||
)
|
||||
buffer.clear()
|
||||
backslashes = 0
|
||||
|
||||
def can_advance() -> bool:
|
||||
return cursor < len(source)
|
||||
|
||||
while can_advance():
|
||||
index = source[cursor]
|
||||
if index in OPERATORS:
|
||||
yield from send_buffer()
|
||||
yield Token(OPERATORS[index], index, cursor, cursor + 1)
|
||||
elif index == BACKSLASH and can_advance():
|
||||
if source[cursor + 1] in SPECIAL_CHARS:
|
||||
backslashes += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
|
||||
buffer.append(source[cursor + 1])
|
||||
cursor += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
|
||||
cursor += 1
|
||||
|
||||
yield from send_buffer()
|
||||
|
||||
|
||||
class PathAction(Enum):
|
||||
KEY = auto()
|
||||
INDEX = auto()
|
||||
APPEND = auto()
|
||||
|
||||
# Pseudo action, used by the interpreter
|
||||
SET = auto()
|
||||
|
||||
def to_string(self) -> str:
|
||||
return self.name.lower()
|
||||
|
||||
|
||||
class Path:
|
||||
def __init__(
|
||||
self,
|
||||
kind: PathAction,
|
||||
accessor: Optional[Union[str, int]] = None,
|
||||
tokens: Optional[List[Token]] = None,
|
||||
is_root: bool = False,
|
||||
):
|
||||
self.kind = kind
|
||||
self.accessor = accessor
|
||||
self.tokens = tokens or []
|
||||
self.is_root = is_root
|
||||
|
||||
def reconstruct(self) -> str:
|
||||
if self.kind is PathAction.KEY:
|
||||
if self.is_root:
|
||||
return str(self.accessor)
|
||||
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.INDEX:
|
||||
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.APPEND:
|
||||
return OPEN_BRACKET + CLOSE_BRACKET
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
|
||||
def parse(source: str) -> Iterator[Path]:
|
||||
"""
|
||||
start: root_path path*
|
||||
root_path: (literal | index_path | append_path)
|
||||
literal: TEXT | NUMBER
|
||||
|
||||
path:
|
||||
key_path
|
||||
| index_path
|
||||
| append_path
|
||||
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
|
||||
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
|
||||
append_path: LEFT_BRACKET RIGHT_BRACKET
|
||||
"""
|
||||
|
||||
tokens = list(tokenize(source))
|
||||
cursor = 0
|
||||
|
||||
def can_advance():
|
||||
return cursor < len(tokens)
|
||||
|
||||
def expect(*kinds):
|
||||
nonlocal cursor
|
||||
|
||||
assert len(kinds) > 0
|
||||
if can_advance():
|
||||
token = tokens[cursor]
|
||||
cursor += 1
|
||||
if token.kind in kinds:
|
||||
return token
|
||||
elif tokens:
|
||||
token = tokens[-1]._replace(
|
||||
start=tokens[-1].end + 0, end=tokens[-1].end + 1
|
||||
)
|
||||
else:
|
||||
token = None
|
||||
|
||||
if len(kinds) == 1:
|
||||
suffix = kinds[0].to_name()
|
||||
else:
|
||||
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
|
||||
suffix += ' or ' + kinds[-1].to_name()
|
||||
|
||||
message = f'Expecting {suffix}'
|
||||
raise HTTPieSyntaxError(source, token, message)
|
||||
|
||||
def parse_root():
|
||||
tokens = []
|
||||
if not can_advance():
|
||||
return Path(
|
||||
PathAction.KEY,
|
||||
EMPTY_STRING,
|
||||
is_root=True
|
||||
)
|
||||
|
||||
# (literal | index_path | append_path)?
|
||||
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
|
||||
tokens.append(token)
|
||||
|
||||
if token.kind in LITERAL_TOKENS:
|
||||
action = PathAction.KEY
|
||||
value = str(token.value)
|
||||
elif token.kind is TokenKind.LEFT_BRACKET:
|
||||
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
|
||||
tokens.append(token)
|
||||
if token.kind is TokenKind.NUMBER:
|
||||
action = PathAction.INDEX
|
||||
value = token.value
|
||||
tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
elif token.kind is TokenKind.RIGHT_BRACKET:
|
||||
action = PathAction.APPEND
|
||||
value = None
|
||||
else:
|
||||
assert_cant_happen()
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
return Path(
|
||||
action,
|
||||
value,
|
||||
tokens=tokens,
|
||||
is_root=True
|
||||
)
|
||||
|
||||
yield parse_root()
|
||||
|
||||
# path*
|
||||
while can_advance():
|
||||
path_tokens = []
|
||||
path_tokens.append(expect(TokenKind.LEFT_BRACKET))
|
||||
|
||||
token = expect(
|
||||
TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET
|
||||
)
|
||||
path_tokens.append(token)
|
||||
if token.kind is TokenKind.RIGHT_BRACKET:
|
||||
path = Path(PathAction.APPEND, tokens=path_tokens)
|
||||
elif token.kind is TokenKind.TEXT:
|
||||
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
elif token.kind is TokenKind.NUMBER:
|
||||
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
else:
|
||||
assert_cant_happen()
|
||||
yield path
|
||||
|
||||
|
||||
JSON_TYPE_MAPPING = {
|
||||
dict: 'object',
|
||||
list: 'array',
|
||||
int: 'number',
|
||||
float: 'number',
|
||||
str: 'string',
|
||||
}
|
||||
|
||||
|
||||
def interpret(context: Any, key: str, value: Any) -> Any:
|
||||
cursor = context
|
||||
|
||||
paths = list(parse(key))
|
||||
paths.append(Path(PathAction.SET, value))
|
||||
|
||||
def type_check(index: int, path: Path, expected_type: Type[Any]) -> None:
|
||||
if not isinstance(cursor, expected_type):
|
||||
if path.tokens:
|
||||
pseudo_token = Token(
|
||||
None, None, path.tokens[0].start, path.tokens[-1].end
|
||||
)
|
||||
else:
|
||||
pseudo_token = None
|
||||
|
||||
cursor_type = JSON_TYPE_MAPPING.get(
|
||||
type(cursor), type(cursor).__name__
|
||||
)
|
||||
required_type = JSON_TYPE_MAPPING[expected_type]
|
||||
|
||||
message = f"Can't perform {path.kind.to_string()!r} based access on "
|
||||
message += repr(
|
||||
''.join(path.reconstruct() for path in paths[:index])
|
||||
)
|
||||
message += (
|
||||
f' which has a type of {cursor_type!r} but this operation'
|
||||
)
|
||||
message += f' requires a type of {required_type!r}.'
|
||||
raise HTTPieSyntaxError(
|
||||
key, pseudo_token, message, message_kind='Type'
|
||||
)
|
||||
|
||||
def object_for(kind: str) -> Any:
|
||||
if kind is PathAction.KEY:
|
||||
return {}
|
||||
elif kind in {PathAction.INDEX, PathAction.APPEND}:
|
||||
return []
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
|
||||
# If there is no context yet, set it.
|
||||
if cursor is None:
|
||||
context = cursor = object_for(path.kind)
|
||||
|
||||
if path.kind is PathAction.KEY:
|
||||
type_check(index, path, dict)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
|
||||
cursor = cursor.setdefault(
|
||||
path.accessor, object_for(next_path.kind)
|
||||
)
|
||||
elif path.kind is PathAction.INDEX:
|
||||
type_check(index, path, list)
|
||||
if path.accessor < 0:
|
||||
raise HTTPieSyntaxError(
|
||||
key,
|
||||
path.tokens[1],
|
||||
'Negative indexes are not supported.',
|
||||
message_kind='Value',
|
||||
)
|
||||
cursor.extend([None] * (path.accessor - len(cursor) + 1))
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
|
||||
if cursor[path.accessor] is None:
|
||||
cursor[path.accessor] = object_for(next_path.kind)
|
||||
|
||||
cursor = cursor[path.accessor]
|
||||
elif path.kind is PathAction.APPEND:
|
||||
type_check(index, path, list)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor.append(next_path.accessor)
|
||||
break
|
||||
|
||||
cursor.append(object_for(next_path.kind))
|
||||
cursor = cursor[-1]
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def wrap_with_dict(context):
|
||||
if context is None:
|
||||
return {}
|
||||
elif isinstance(context, list):
|
||||
return {EMPTY_STRING: NestedJSONArray(context)}
|
||||
else:
|
||||
assert isinstance(context, dict)
|
||||
return context
|
||||
|
||||
|
||||
def interpret_nested_json(pairs):
|
||||
context = None
|
||||
for key, value in pairs:
|
||||
context = interpret(context, key, value)
|
||||
|
||||
return wrap_with_dict(context)
|
20
httpie/cli/nested_json/__init__.py
Normal file
20
httpie/cli/nested_json/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""
|
||||
A library for parsing the HTTPie nested JSON key syntax and constructing the resulting objects.
|
||||
|
||||
<https://httpie.io/docs/cli/nested-json>
|
||||
|
||||
It has no dependencies.
|
||||
|
||||
"""
|
||||
from .interpret import interpret_nested_json, unwrap_top_level_list_if_needed
|
||||
from .errors import NestedJSONSyntaxError
|
||||
from .tokens import EMPTY_STRING, NestedJSONArray
|
||||
|
||||
|
||||
__all__ = [
|
||||
'interpret_nested_json',
|
||||
'unwrap_top_level_list_if_needed',
|
||||
'EMPTY_STRING',
|
||||
'NestedJSONArray',
|
||||
'NestedJSONSyntaxError'
|
||||
]
|
27
httpie/cli/nested_json/errors.py
Normal file
27
httpie/cli/nested_json/errors.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import Optional
|
||||
|
||||
from .tokens import Token, HIGHLIGHTER
|
||||
|
||||
|
||||
class NestedJSONSyntaxError(ValueError):
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
token: Optional[Token],
|
||||
message: str,
|
||||
message_kind: str = 'Syntax',
|
||||
) -> None:
|
||||
self.source = source
|
||||
self.token = token
|
||||
self.message = message
|
||||
self.message_kind = message_kind
|
||||
|
||||
def __str__(self):
|
||||
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
|
||||
if self.token is not None:
|
||||
lines.append(self.source)
|
||||
lines.append(
|
||||
' ' * self.token.start
|
||||
+ HIGHLIGHTER * (self.token.end - self.token.start)
|
||||
)
|
||||
return '\n'.join(lines)
|
129
httpie/cli/nested_json/interpret.py
Normal file
129
httpie/cli/nested_json/interpret.py
Normal file
@ -0,0 +1,129 @@
|
||||
from typing import Type, Union, Any, Iterable, Tuple
|
||||
|
||||
from .parse import parse, assert_cant_happen
|
||||
from .errors import NestedJSONSyntaxError
|
||||
from .tokens import EMPTY_STRING, TokenKind, Token, PathAction, Path, NestedJSONArray
|
||||
|
||||
|
||||
__all__ = [
|
||||
'interpret_nested_json',
|
||||
'unwrap_top_level_list_if_needed',
|
||||
]
|
||||
|
||||
JSONType = Type[Union[dict, list, int, float, str]]
|
||||
JSON_TYPE_MAPPING = {
|
||||
dict: 'object',
|
||||
list: 'array',
|
||||
int: 'number',
|
||||
float: 'number',
|
||||
str: 'string',
|
||||
}
|
||||
|
||||
|
||||
def interpret_nested_json(pairs: Iterable[Tuple[str, str]]) -> dict:
|
||||
context = None
|
||||
for key, value in pairs:
|
||||
context = interpret(context, key, value)
|
||||
return wrap_with_dict(context)
|
||||
|
||||
|
||||
def interpret(context: Any, key: str, value: Any) -> Any:
|
||||
cursor = context
|
||||
paths = list(parse(key))
|
||||
paths.append(Path(PathAction.SET, value))
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def type_check(index: int, path: Path, expected_type: JSONType):
|
||||
if not isinstance(cursor, expected_type):
|
||||
if path.tokens:
|
||||
pseudo_token = Token(
|
||||
kind=TokenKind.PSEUDO,
|
||||
value='',
|
||||
start=path.tokens[0].start,
|
||||
end=path.tokens[-1].end,
|
||||
)
|
||||
else:
|
||||
pseudo_token = None
|
||||
cursor_type = JSON_TYPE_MAPPING.get(type(cursor), type(cursor).__name__)
|
||||
required_type = JSON_TYPE_MAPPING[expected_type]
|
||||
message = f'Cannot perform {path.kind.to_string()!r} based access on '
|
||||
message += repr(''.join(path.reconstruct() for path in paths[:index]))
|
||||
message += f' which has a type of {cursor_type!r} but this operation'
|
||||
message += f' requires a type of {required_type!r}.'
|
||||
raise NestedJSONSyntaxError(
|
||||
source=key,
|
||||
token=pseudo_token,
|
||||
message=message,
|
||||
message_kind='Type',
|
||||
)
|
||||
|
||||
def object_for(kind: PathAction) -> Any:
|
||||
if kind is PathAction.KEY:
|
||||
return {}
|
||||
elif kind in {PathAction.INDEX, PathAction.APPEND}:
|
||||
return []
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
|
||||
# If there is no context yet, set it.
|
||||
if cursor is None:
|
||||
context = cursor = object_for(path.kind)
|
||||
if path.kind is PathAction.KEY:
|
||||
type_check(index, path, dict)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
cursor = cursor.setdefault(path.accessor, object_for(next_path.kind))
|
||||
elif path.kind is PathAction.INDEX:
|
||||
type_check(index, path, list)
|
||||
if path.accessor < 0:
|
||||
raise NestedJSONSyntaxError(
|
||||
source=key,
|
||||
token=path.tokens[1],
|
||||
message='Negative indexes are not supported.',
|
||||
message_kind='Value',
|
||||
)
|
||||
cursor.extend([None] * (path.accessor - len(cursor) + 1))
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
if cursor[path.accessor] is None:
|
||||
cursor[path.accessor] = object_for(next_path.kind)
|
||||
cursor = cursor[path.accessor]
|
||||
elif path.kind is PathAction.APPEND:
|
||||
type_check(index, path, list)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor.append(next_path.accessor)
|
||||
break
|
||||
cursor.append(object_for(next_path.kind))
|
||||
cursor = cursor[-1]
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def wrap_with_dict(context):
|
||||
if context is None:
|
||||
return {}
|
||||
elif isinstance(context, list):
|
||||
return {
|
||||
EMPTY_STRING: NestedJSONArray(context),
|
||||
}
|
||||
else:
|
||||
assert isinstance(context, dict)
|
||||
return context
|
||||
|
||||
|
||||
def unwrap_top_level_list_if_needed(data: dict):
|
||||
"""
|
||||
Propagate the top-level list, if that’s what we got.
|
||||
|
||||
"""
|
||||
if len(data) == 1:
|
||||
key, value = list(data.items())[0]
|
||||
if isinstance(value, NestedJSONArray):
|
||||
assert key == EMPTY_STRING
|
||||
return value
|
||||
return data
|
193
httpie/cli/nested_json/parse.py
Normal file
193
httpie/cli/nested_json/parse.py
Normal file
@ -0,0 +1,193 @@
|
||||
from typing import Iterator
|
||||
|
||||
from .errors import NestedJSONSyntaxError
|
||||
from .tokens import (
|
||||
EMPTY_STRING,
|
||||
BACKSLASH,
|
||||
TokenKind,
|
||||
OPERATORS,
|
||||
SPECIAL_CHARS,
|
||||
LITERAL_TOKENS,
|
||||
Token,
|
||||
PathAction,
|
||||
Path,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'parse',
|
||||
'assert_cant_happen',
|
||||
]
|
||||
|
||||
|
||||
def parse(source: str) -> Iterator[Path]:
|
||||
"""
|
||||
start: root_path path*
|
||||
root_path: (literal | index_path | append_path)
|
||||
literal: TEXT | NUMBER
|
||||
|
||||
path:
|
||||
key_path
|
||||
| index_path
|
||||
| append_path
|
||||
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
|
||||
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
|
||||
append_path: LEFT_BRACKET RIGHT_BRACKET
|
||||
|
||||
"""
|
||||
|
||||
tokens = list(tokenize(source))
|
||||
cursor = 0
|
||||
|
||||
def can_advance():
|
||||
return cursor < len(tokens)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def expect(*kinds):
|
||||
nonlocal cursor
|
||||
assert kinds
|
||||
if can_advance():
|
||||
token = tokens[cursor]
|
||||
cursor += 1
|
||||
if token.kind in kinds:
|
||||
return token
|
||||
elif tokens:
|
||||
token = tokens[-1]._replace(
|
||||
start=tokens[-1].end + 0,
|
||||
end=tokens[-1].end + 1,
|
||||
)
|
||||
else:
|
||||
token = None
|
||||
if len(kinds) == 1:
|
||||
suffix = kinds[0].to_name()
|
||||
else:
|
||||
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
|
||||
suffix += ' or ' + kinds[-1].to_name()
|
||||
message = f'Expecting {suffix}'
|
||||
raise NestedJSONSyntaxError(source, token, message)
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def parse_root():
|
||||
tokens = []
|
||||
if not can_advance():
|
||||
return Path(
|
||||
kind=PathAction.KEY,
|
||||
accessor=EMPTY_STRING,
|
||||
is_root=True
|
||||
)
|
||||
# (literal | index_path | append_path)?
|
||||
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
|
||||
tokens.append(token)
|
||||
if token.kind in LITERAL_TOKENS:
|
||||
action = PathAction.KEY
|
||||
value = str(token.value)
|
||||
elif token.kind is TokenKind.LEFT_BRACKET:
|
||||
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
|
||||
tokens.append(token)
|
||||
if token.kind is TokenKind.NUMBER:
|
||||
action = PathAction.INDEX
|
||||
value = token.value
|
||||
tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
elif token.kind is TokenKind.RIGHT_BRACKET:
|
||||
action = PathAction.APPEND
|
||||
value = None
|
||||
else:
|
||||
assert_cant_happen()
|
||||
else:
|
||||
assert_cant_happen()
|
||||
# noinspection PyUnboundLocalVariable
|
||||
return Path(
|
||||
kind=action,
|
||||
accessor=value,
|
||||
tokens=tokens,
|
||||
is_root=True
|
||||
)
|
||||
|
||||
yield parse_root()
|
||||
|
||||
# path*
|
||||
while can_advance():
|
||||
path_tokens = [expect(TokenKind.LEFT_BRACKET)]
|
||||
token = expect(TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
|
||||
path_tokens.append(token)
|
||||
if token.kind is TokenKind.RIGHT_BRACKET:
|
||||
path = Path(PathAction.APPEND, tokens=path_tokens)
|
||||
elif token.kind is TokenKind.TEXT:
|
||||
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
elif token.kind is TokenKind.NUMBER:
|
||||
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
else:
|
||||
assert_cant_happen()
|
||||
# noinspection PyUnboundLocalVariable
|
||||
yield path
|
||||
|
||||
|
||||
def tokenize(source: str) -> Iterator[Token]:
|
||||
cursor = 0
|
||||
backslashes = 0
|
||||
buffer = []
|
||||
|
||||
def send_buffer() -> Iterator[Token]:
|
||||
nonlocal backslashes
|
||||
if not buffer:
|
||||
return None
|
||||
|
||||
value = ''.join(buffer)
|
||||
kind = TokenKind.TEXT
|
||||
if not backslashes:
|
||||
for variation, kind in [
|
||||
(int, TokenKind.NUMBER),
|
||||
(check_escaped_int, TokenKind.TEXT),
|
||||
]:
|
||||
try:
|
||||
value = variation(value)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
yield Token(
|
||||
kind=kind,
|
||||
value=value,
|
||||
start=cursor - (len(buffer) + backslashes),
|
||||
end=cursor,
|
||||
)
|
||||
buffer.clear()
|
||||
backslashes = 0
|
||||
|
||||
def can_advance() -> bool:
|
||||
return cursor < len(source)
|
||||
|
||||
while can_advance():
|
||||
index = source[cursor]
|
||||
if index in OPERATORS:
|
||||
yield from send_buffer()
|
||||
yield Token(OPERATORS[index], index, cursor, cursor + 1)
|
||||
elif index == BACKSLASH and can_advance():
|
||||
if source[cursor + 1] in SPECIAL_CHARS:
|
||||
backslashes += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
buffer.append(source[cursor + 1])
|
||||
cursor += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
cursor += 1
|
||||
|
||||
yield from send_buffer()
|
||||
|
||||
|
||||
def check_escaped_int(value: str) -> str:
|
||||
if not value.startswith(BACKSLASH):
|
||||
raise ValueError('Not an escaped int')
|
||||
try:
|
||||
int(value[1:])
|
||||
except ValueError as exc:
|
||||
raise ValueError('Not an escaped int') from exc
|
||||
else:
|
||||
return value[1:]
|
||||
|
||||
|
||||
def assert_cant_happen():
|
||||
raise ValueError('Unexpected value')
|
80
httpie/cli/nested_json/tokens.py
Normal file
80
httpie/cli/nested_json/tokens.py
Normal file
@ -0,0 +1,80 @@
|
||||
from enum import Enum, auto
|
||||
from typing import NamedTuple, Union, Optional, List
|
||||
|
||||
EMPTY_STRING = ''
|
||||
HIGHLIGHTER = '^'
|
||||
OPEN_BRACKET = '['
|
||||
CLOSE_BRACKET = ']'
|
||||
BACKSLASH = '\\'
|
||||
|
||||
|
||||
class TokenKind(Enum):
|
||||
TEXT = auto()
|
||||
NUMBER = auto()
|
||||
LEFT_BRACKET = auto()
|
||||
RIGHT_BRACKET = auto()
|
||||
PSEUDO = auto() # Not a real token, use when representing location only.
|
||||
|
||||
def to_name(self) -> str:
|
||||
for key, value in OPERATORS.items():
|
||||
if value is self:
|
||||
return repr(key)
|
||||
else:
|
||||
return 'a ' + self.name.lower()
|
||||
|
||||
|
||||
OPERATORS = {
|
||||
OPEN_BRACKET: TokenKind.LEFT_BRACKET,
|
||||
CLOSE_BRACKET: TokenKind.RIGHT_BRACKET,
|
||||
}
|
||||
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
|
||||
LITERAL_TOKENS = [
|
||||
TokenKind.TEXT,
|
||||
TokenKind.NUMBER,
|
||||
]
|
||||
|
||||
|
||||
class Token(NamedTuple):
|
||||
kind: TokenKind
|
||||
value: Union[str, int]
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
class PathAction(Enum):
|
||||
KEY = auto()
|
||||
INDEX = auto()
|
||||
APPEND = auto()
|
||||
# Pseudo action, used by the interpreter
|
||||
SET = auto()
|
||||
|
||||
def to_string(self) -> str:
|
||||
return self.name.lower()
|
||||
|
||||
|
||||
class Path:
|
||||
def __init__(
|
||||
self,
|
||||
kind: PathAction,
|
||||
accessor: Optional[Union[str, int]] = None,
|
||||
tokens: Optional[List[Token]] = None,
|
||||
is_root: bool = False,
|
||||
):
|
||||
self.kind = kind
|
||||
self.accessor = accessor
|
||||
self.tokens = tokens or []
|
||||
self.is_root = is_root
|
||||
|
||||
def reconstruct(self) -> str:
|
||||
if self.kind is PathAction.KEY:
|
||||
if self.is_root:
|
||||
return str(self.accessor)
|
||||
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.INDEX:
|
||||
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.APPEND:
|
||||
return OPEN_BRACKET + CLOSE_BRACKET
|
||||
|
||||
|
||||
class NestedJSONArray(list):
|
||||
"""Denotes a top-level JSON array."""
|
@ -35,17 +35,6 @@ def drop_keys(
|
||||
}
|
||||
|
||||
|
||||
def _get_first_line(source: str) -> str:
|
||||
parts = []
|
||||
for line in source.strip().splitlines():
|
||||
line = line.strip()
|
||||
parts.append(line)
|
||||
if line.endswith("."):
|
||||
break
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
PARSER_SPEC_VERSION = '0.0.1a0'
|
||||
|
||||
|
||||
@ -55,6 +44,8 @@ class ParserSpec:
|
||||
description: Optional[str] = None
|
||||
epilog: Optional[str] = None
|
||||
groups: List['Group'] = field(default_factory=list)
|
||||
man_page_hint: Optional[str] = None
|
||||
source_file: Optional[str] = None
|
||||
|
||||
def finalize(self) -> 'ParserSpec':
|
||||
if self.description:
|
||||
@ -248,10 +239,11 @@ def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
||||
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
|
||||
|
||||
|
||||
def parser_to_parser_spec(parser: argparse.ArgumentParser) -> ParserSpec:
|
||||
def parser_to_parser_spec(parser: argparse.ArgumentParser, **kwargs) -> ParserSpec:
|
||||
"""Take an existing argparse parser, and create a spec from it."""
|
||||
return ParserSpec(
|
||||
program=parser.prog,
|
||||
description=parser.description,
|
||||
epilog=parser.epilog
|
||||
epilog=parser.epilog,
|
||||
**kwargs
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ from .dicts import (
|
||||
)
|
||||
from .exceptions import ParseError
|
||||
from .nested_json import interpret_nested_json
|
||||
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split
|
||||
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split_iterable
|
||||
|
||||
|
||||
class RequestItems:
|
||||
@ -78,25 +78,28 @@ class RequestItems:
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_RAW_JSON: (
|
||||
json_only(instance, process_data_raw_json_embed_arg),
|
||||
convert_json_value_to_form_if_needed(
|
||||
in_json_mode=instance.is_json,
|
||||
processor=process_data_raw_json_embed_arg
|
||||
),
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
|
||||
json_only(instance, process_data_embed_raw_json_file_arg),
|
||||
convert_json_value_to_form_if_needed(
|
||||
in_json_mode=instance.is_json,
|
||||
processor=process_data_embed_raw_json_file_arg,
|
||||
),
|
||||
instance.data,
|
||||
),
|
||||
}
|
||||
|
||||
if instance.is_json:
|
||||
json_item_args, request_item_args = split(
|
||||
request_item_args,
|
||||
lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
|
||||
json_item_args, request_item_args = split_iterable(
|
||||
iterable=request_item_args,
|
||||
key=lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
|
||||
)
|
||||
if json_item_args:
|
||||
pairs = [
|
||||
(arg.key, rules[arg.sep][0](arg))
|
||||
for arg in json_item_args
|
||||
]
|
||||
pairs = [(arg.key, rules[arg.sep][0](arg)) for arg in json_item_args]
|
||||
processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS]
|
||||
value = processor_func(pairs)
|
||||
target_dict.update(value)
|
||||
@ -159,6 +162,30 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
)
|
||||
|
||||
|
||||
def convert_json_value_to_form_if_needed(in_json_mode: bool, processor: Callable[[KeyValueArg], JSONType]) -> Callable[[], str]:
|
||||
"""
|
||||
We allow primitive values to be passed to forms via JSON key/value syntax.
|
||||
|
||||
But complex values lead to an error because there’s no clear way to serialize them.
|
||||
|
||||
"""
|
||||
if in_json_mode:
|
||||
return processor
|
||||
|
||||
@functools.wraps(processor)
|
||||
def wrapper(*args, **kwargs) -> str:
|
||||
try:
|
||||
output = processor(*args, **kwargs)
|
||||
except ParseError:
|
||||
output = None
|
||||
if isinstance(output, (str, int, float)):
|
||||
return str(output)
|
||||
else:
|
||||
raise ParseError('Cannot use complex JSON value types with --form/--multipart.')
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def process_data_item_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
@ -167,29 +194,6 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg)
|
||||
|
||||
|
||||
def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
|
||||
if items.is_json:
|
||||
return func
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs) -> str:
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
except ParseError:
|
||||
ret = None
|
||||
|
||||
# If it is a basic type, then allow it
|
||||
if isinstance(ret, (str, int, float)):
|
||||
return str(ret)
|
||||
else:
|
||||
raise ParseError(
|
||||
'Can\'t use complex JSON value types with '
|
||||
'--form/--multipart.'
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
|
||||
contents = load_text_file(arg)
|
||||
value = load_json(arg, contents)
|
||||
|
@ -10,11 +10,13 @@ from urllib.parse import urlparse, urlunparse
|
||||
import requests
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
|
||||
from . import __version__
|
||||
from .adapters import HTTPieHTTPAdapter
|
||||
from .cli.constants import HTTP_OPTIONS
|
||||
from .cli.dicts import HTTPHeadersDict
|
||||
from .cli.nested_json import unwrap_top_level_list_if_needed
|
||||
from .context import Environment
|
||||
from .cli.constants import EMPTY_STRING, HTTP_OPTIONS
|
||||
from .cli.dicts import HTTPHeadersDict, NestedJSONArray
|
||||
from .encoding import UTF8
|
||||
from .models import RequestsMessage
|
||||
from .plugins.registry import plugin_manager
|
||||
@ -305,21 +307,13 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
|
||||
|
||||
|
||||
def json_dict_to_request_body(data: Dict[str, Any]) -> str:
|
||||
# Propagate the top-level list if there is only one
|
||||
# item in the object, with an en empty key.
|
||||
if len(data) == 1:
|
||||
[(key, value)] = data.items()
|
||||
if isinstance(value, NestedJSONArray):
|
||||
assert key == EMPTY_STRING
|
||||
data = value
|
||||
|
||||
data = unwrap_top_level_list_if_needed(data)
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
else:
|
||||
# We need to set data to an empty string to prevent requests
|
||||
# from assigning an empty list to `response.request.data`.
|
||||
data = ''
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
@ -149,6 +149,24 @@ class Config(BaseConfigDict):
|
||||
def default_options(self) -> list:
|
||||
return self['default_options']
|
||||
|
||||
def _configured_path(self, config_option: str, default: str) -> None:
|
||||
return Path(
|
||||
self.get(config_option, self.directory / default)
|
||||
).expanduser().resolve()
|
||||
|
||||
@property
|
||||
def plugins_dir(self) -> Path:
|
||||
return Path(self.get('plugins_dir', self.directory / 'plugins')).resolve()
|
||||
return self._configured_path('plugins_dir', 'plugins')
|
||||
|
||||
@property
|
||||
def version_info_file(self) -> Path:
|
||||
return self._configured_path('version_info_file', 'version_info.json')
|
||||
|
||||
@property
|
||||
def developer_mode(self) -> bool:
|
||||
"""This is a special setting for the development environment. It is
|
||||
different from the --debug mode in the terms that it might change
|
||||
the behavior for certain parameters (e.g updater system) that
|
||||
we usually ignore."""
|
||||
|
||||
return self.get('developer_mode')
|
||||
|
@ -145,7 +145,7 @@ class Environment:
|
||||
try:
|
||||
config.load()
|
||||
except ConfigFileError as e:
|
||||
self.log_error(e, level='warning')
|
||||
self.log_error(e, level=LogLevel.WARNING)
|
||||
return config
|
||||
|
||||
@property
|
||||
@ -174,7 +174,7 @@ class Environment:
|
||||
stderr = self._orig_stderr
|
||||
rich_console = self._make_rich_console(file=stderr, force_terminal=stderr.isatty())
|
||||
rich_console.print(
|
||||
f'\n{self.program_name}: {level}: {msg}\n\n',
|
||||
f'\n{self.program_name}: {level.value}: {msg}\n\n',
|
||||
style=LOG_LEVEL_COLORS[level],
|
||||
markup=False,
|
||||
highlight=False,
|
||||
|
@ -11,7 +11,7 @@ from requests import __version__ as requests_version
|
||||
|
||||
from . import __version__ as httpie_version
|
||||
from .cli.constants import OUT_REQ_BODY
|
||||
from .cli.nested_json import HTTPieSyntaxError
|
||||
from .cli.nested_json import NestedJSONSyntaxError
|
||||
from .client import collect_messages
|
||||
from .context import Environment, LogLevel
|
||||
from .downloads import Downloader
|
||||
@ -24,6 +24,8 @@ from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_
|
||||
from .plugins.registry import plugin_manager
|
||||
from .status import ExitStatus, http_status_to_exit_status
|
||||
from .utils import unwrap_context
|
||||
from .internal.update_warnings import check_updates
|
||||
from .internal.daemon_runner import is_daemon_mode, run_daemon_task
|
||||
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
@ -37,6 +39,10 @@ def raw_main(
|
||||
program_name, *args = args
|
||||
env.program_name = os.path.basename(program_name)
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
|
||||
if is_daemon_mode(args):
|
||||
return run_daemon_task(env, args)
|
||||
|
||||
plugin_manager.load_installed_plugins(env.config.plugins_dir)
|
||||
|
||||
if use_default_options and env.config.default_options:
|
||||
@ -72,7 +78,7 @@ def raw_main(
|
||||
args=args,
|
||||
env=env,
|
||||
)
|
||||
except HTTPieSyntaxError as exc:
|
||||
except NestedJSONSyntaxError as exc:
|
||||
env.stderr.write(str(exc) + "\n")
|
||||
if include_traceback:
|
||||
raise
|
||||
@ -89,6 +95,7 @@ def raw_main(
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
else:
|
||||
check_updates(env)
|
||||
try:
|
||||
exit_status = main_program(
|
||||
args=parsed_args,
|
||||
|
5
httpie/internal/__build_channel__.py
Normal file
5
httpie/internal/__build_channel__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# Represents the packaging method. This file should
|
||||
# be overridden by every build system we support on
|
||||
# the packaging step.
|
||||
|
||||
BUILD_CHANNEL = 'unknown'
|
0
httpie/internal/__init__.py
Normal file
0
httpie/internal/__init__.py
Normal file
50
httpie/internal/daemon_runner.py
Normal file
50
httpie/internal/daemon_runner.py
Normal file
@ -0,0 +1,50 @@
|
||||
import argparse
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from typing import List
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.internal.update_warnings import _fetch_updates, _get_suppress_context
|
||||
from httpie.status import ExitStatus
|
||||
|
||||
STATUS_FILE = '.httpie-test-daemon-status'
|
||||
|
||||
|
||||
def _check_status(env):
|
||||
# This function is used only for the testing (test_update_warnings).
|
||||
# Since we don't want to trigger the fetch_updates (which would interact
|
||||
# with real world resources), we'll only trigger this pseudo task
|
||||
# and check whether the STATUS_FILE is created or not.
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
status_file = Path(tempfile.gettempdir()) / STATUS_FILE
|
||||
status_file.touch()
|
||||
|
||||
|
||||
DAEMONIZED_TASKS = {
|
||||
'check_status': _check_status,
|
||||
'fetch_updates': _fetch_updates,
|
||||
}
|
||||
|
||||
|
||||
def _parse_options(args: List[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('task_id')
|
||||
parser.add_argument('--daemon', action='store_true')
|
||||
return parser.parse_known_args(args)[0]
|
||||
|
||||
|
||||
def is_daemon_mode(args: List[str]) -> bool:
|
||||
return '--daemon' in args
|
||||
|
||||
|
||||
def run_daemon_task(env: Environment, args: List[str]) -> ExitStatus:
|
||||
options = _parse_options(args)
|
||||
|
||||
assert options.daemon
|
||||
assert options.task_id in DAEMONIZED_TASKS
|
||||
with redirect_stdout(env.devnull), redirect_stderr(env.devnull):
|
||||
with _get_suppress_context(env):
|
||||
DAEMONIZED_TASKS[options.task_id](env)
|
||||
|
||||
return ExitStatus.SUCCESS
|
121
httpie/internal/daemons.py
Normal file
121
httpie/internal/daemons.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""
|
||||
This module provides an interface to spawn a detached task to be
|
||||
run with httpie.internal.daemon_runner on a separate process. It is
|
||||
based on DVC's daemon system.
|
||||
https://github.com/iterative/dvc/blob/main/dvc/daemon.py
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import httpie.__main__
|
||||
from contextlib import suppress
|
||||
from subprocess import Popen, DEVNULL
|
||||
from typing import Dict, List
|
||||
from httpie.compat import is_frozen, is_windows
|
||||
|
||||
|
||||
ProcessContext = Dict[str, str]
|
||||
|
||||
|
||||
def _start_process(cmd: List[str], **kwargs) -> Popen:
|
||||
prefix = [sys.executable]
|
||||
# If it is frozen, sys.executable points to the binary (http).
|
||||
# Otherwise it points to the python interpreter.
|
||||
if not is_frozen:
|
||||
main_entrypoint = httpie.__main__.__file__
|
||||
prefix += [main_entrypoint]
|
||||
return Popen(prefix + cmd, close_fds=True, shell=False, stdout=DEVNULL, stderr=DEVNULL, **kwargs)
|
||||
|
||||
|
||||
def _spawn_windows(cmd: List[str], process_context: ProcessContext) -> None:
|
||||
from subprocess import (
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
STARTUPINFO,
|
||||
)
|
||||
|
||||
# https://stackoverflow.com/a/7006424
|
||||
# https://bugs.python.org/issue41619
|
||||
creationflags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
|
||||
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_start_process(
|
||||
cmd,
|
||||
env=process_context,
|
||||
creationflags=creationflags,
|
||||
startupinfo=startupinfo,
|
||||
)
|
||||
|
||||
|
||||
def _spawn_posix(args: List[str], process_context: ProcessContext) -> None:
|
||||
"""
|
||||
Perform a double fork procedure* to detach from the parent
|
||||
process so that we don't block the user even if their original
|
||||
command's execution is done but the release fetcher is not.
|
||||
|
||||
[1]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11_01_03
|
||||
"""
|
||||
|
||||
from httpie.core import main
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
return
|
||||
except OSError:
|
||||
os._exit(1)
|
||||
|
||||
os.setsid()
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
except OSError:
|
||||
os._exit(1)
|
||||
|
||||
# Close all standard inputs/outputs
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
sys.stderr.close()
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
# Double-fork is not reliable on MacOS, so we'll use a subprocess
|
||||
# to ensure the task is isolated properly.
|
||||
process = _start_process(args, env=process_context)
|
||||
# Unlike windows, since we already completed the fork procedure
|
||||
# we can simply join the process and wait for it.
|
||||
process.communicate()
|
||||
else:
|
||||
os.environ.update(process_context)
|
||||
with suppress(BaseException):
|
||||
main(['http'] + args)
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def _spawn(args: List[str], process_context: ProcessContext) -> None:
|
||||
"""
|
||||
Spawn a new process to run the given command.
|
||||
"""
|
||||
if is_windows:
|
||||
_spawn_windows(args, process_context)
|
||||
else:
|
||||
_spawn_posix(args, process_context)
|
||||
|
||||
|
||||
def spawn_daemon(task: str) -> None:
|
||||
args = [task, '--daemon']
|
||||
process_context = os.environ.copy()
|
||||
if not is_frozen:
|
||||
file_path = os.path.abspath(inspect.stack()[0][1])
|
||||
process_context['PYTHONPATH'] = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(file_path))
|
||||
)
|
||||
|
||||
_spawn(args, process_context)
|
171
httpie/internal/update_warnings.py
Normal file
171
httpie/internal/update_warnings.py
Normal file
@ -0,0 +1,171 @@
|
||||
import json
|
||||
from contextlib import nullcontext, suppress
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Callable
|
||||
|
||||
import requests
|
||||
|
||||
import httpie
|
||||
from httpie.context import Environment, LogLevel
|
||||
from httpie.internal.__build_channel__ import BUILD_CHANNEL
|
||||
from httpie.internal.daemons import spawn_daemon
|
||||
from httpie.utils import is_version_greater, open_with_lockfile
|
||||
|
||||
# Automatically updated package version index.
|
||||
PACKAGE_INDEX_LINK = 'https://packages.httpie.io/latest.json'
|
||||
|
||||
FETCH_INTERVAL = timedelta(weeks=2)
|
||||
WARN_INTERVAL = timedelta(weeks=1)
|
||||
|
||||
UPDATE_MESSAGE_FORMAT = """\
|
||||
A new HTTPie release ({last_released_version}) is available.
|
||||
To see how you can update, please visit https://httpie.io/docs/cli/{installation_method}
|
||||
"""
|
||||
|
||||
ALREADY_UP_TO_DATE_MESSAGE = """\
|
||||
You are already up-to-date.
|
||||
"""
|
||||
|
||||
|
||||
def _read_data_error_free(file: Path) -> Any:
|
||||
# If the file is broken / non-existent, ignore it.
|
||||
try:
|
||||
with open(file) as stream:
|
||||
return json.load(stream)
|
||||
except (ValueError, OSError):
|
||||
return {}
|
||||
|
||||
|
||||
def _fetch_updates(env: Environment) -> str:
|
||||
file = env.config.version_info_file
|
||||
data = _read_data_error_free(file)
|
||||
|
||||
response = requests.get(PACKAGE_INDEX_LINK, verify=False)
|
||||
response.raise_for_status()
|
||||
|
||||
data.setdefault('last_warned_date', None)
|
||||
data['last_fetched_date'] = datetime.now().isoformat()
|
||||
data['last_released_versions'] = response.json()
|
||||
|
||||
with open_with_lockfile(file, 'w') as stream:
|
||||
json.dump(data, stream)
|
||||
|
||||
|
||||
def fetch_updates(env: Environment, lazy: bool = True):
|
||||
if lazy:
|
||||
spawn_daemon('fetch_updates')
|
||||
else:
|
||||
_fetch_updates(env)
|
||||
|
||||
|
||||
def maybe_fetch_updates(env: Environment) -> None:
|
||||
if env.config.get('disable_update_warnings'):
|
||||
return None
|
||||
|
||||
data = _read_data_error_free(env.config.version_info_file)
|
||||
|
||||
if data:
|
||||
current_date = datetime.now()
|
||||
last_fetched_date = datetime.fromisoformat(data['last_fetched_date'])
|
||||
earliest_fetch_date = last_fetched_date + FETCH_INTERVAL
|
||||
if current_date < earliest_fetch_date:
|
||||
return None
|
||||
|
||||
fetch_updates(env)
|
||||
|
||||
|
||||
def _get_suppress_context(env: Environment) -> Any:
|
||||
"""Return a context manager that suppress
|
||||
all possible errors.
|
||||
|
||||
Note: if you have set the developer_mode=True in
|
||||
your config, then it will show all errors for easier
|
||||
debugging."""
|
||||
if env.config.developer_mode:
|
||||
return nullcontext()
|
||||
else:
|
||||
return suppress(BaseException)
|
||||
|
||||
|
||||
def _update_checker(
|
||||
func: Callable[[Environment], None]
|
||||
) -> Callable[[Environment], None]:
|
||||
"""Control the execution of the update checker (suppress errors, trigger
|
||||
auto updates etc.)"""
|
||||
|
||||
def wrapper(env: Environment) -> None:
|
||||
with _get_suppress_context(env):
|
||||
func(env)
|
||||
|
||||
with _get_suppress_context(env):
|
||||
maybe_fetch_updates(env)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _get_update_status(env: Environment) -> Optional[str]:
|
||||
"""If there is a new update available, return the warning text.
|
||||
Otherwise just return None."""
|
||||
file = env.config.version_info_file
|
||||
if not file.exists():
|
||||
return None
|
||||
|
||||
with _get_suppress_context(env):
|
||||
# If the user quickly spawns multiple httpie processes
|
||||
# we don't want to end in a race.
|
||||
with open_with_lockfile(file) as stream:
|
||||
version_info = json.load(stream)
|
||||
|
||||
available_channels = version_info['last_released_versions']
|
||||
if BUILD_CHANNEL not in available_channels:
|
||||
return None
|
||||
|
||||
current_version = httpie.__version__
|
||||
last_released_version = available_channels[BUILD_CHANNEL]
|
||||
if not is_version_greater(last_released_version, current_version):
|
||||
return None
|
||||
|
||||
text = UPDATE_MESSAGE_FORMAT.format(
|
||||
last_released_version=last_released_version,
|
||||
installation_method=BUILD_CHANNEL,
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def get_update_status(env: Environment) -> str:
|
||||
return _get_update_status(env) or ALREADY_UP_TO_DATE_MESSAGE
|
||||
|
||||
|
||||
@_update_checker
|
||||
def check_updates(env: Environment) -> None:
|
||||
if env.config.get('disable_update_warnings'):
|
||||
return None
|
||||
|
||||
file = env.config.version_info_file
|
||||
update_status = _get_update_status(env)
|
||||
|
||||
if not update_status:
|
||||
return None
|
||||
|
||||
# If the user quickly spawns multiple httpie processes
|
||||
# we don't want to end in a race.
|
||||
with open_with_lockfile(file) as stream:
|
||||
version_info = json.load(stream)
|
||||
|
||||
# We don't want to spam the user with too many warnings,
|
||||
# so we'll only warn every once a while (WARN_INTERNAL).
|
||||
current_date = datetime.now()
|
||||
last_warned_date = version_info['last_warned_date']
|
||||
if last_warned_date is not None:
|
||||
earliest_warn_date = (
|
||||
datetime.fromisoformat(last_warned_date) + WARN_INTERVAL
|
||||
)
|
||||
if current_date < earliest_warn_date:
|
||||
return None
|
||||
|
||||
env.log_error(update_status, level=LogLevel.INFO)
|
||||
version_info['last_warned_date'] = current_date.isoformat()
|
||||
|
||||
with open_with_lockfile(file, 'w') as stream:
|
||||
json.dump(version_info, stream)
|
@ -52,7 +52,6 @@ def program():
|
||||
try:
|
||||
exit_status = main()
|
||||
except KeyboardInterrupt:
|
||||
from httpie.status import ExitStatus
|
||||
exit_status = ExitStatus.ERROR_CTRL_C
|
||||
|
||||
return exit_status
|
||||
|
@ -20,9 +20,13 @@ COMMANDS = {
|
||||
{
|
||||
'flags': ['-f', '--format'],
|
||||
'choices': ['json'],
|
||||
'help': 'Format to export in.',
|
||||
'default': 'json'
|
||||
}
|
||||
],
|
||||
'check-updates': [
|
||||
'Check for updates'
|
||||
],
|
||||
'sessions': {
|
||||
'help': 'Manage HTTPie sessions',
|
||||
'upgrade': [
|
||||
@ -166,5 +170,12 @@ parser.add_argument(
|
||||
'''
|
||||
)
|
||||
|
||||
options = parser_to_parser_spec(parser)
|
||||
man_page_hint = '''
|
||||
If you are looking for the man pages of http/https commands, try one of the following:
|
||||
$ man http
|
||||
$ man https
|
||||
|
||||
'''
|
||||
|
||||
options = parser_to_parser_spec(parser, man_page_hint=man_page_hint, source_file=__file__)
|
||||
generate_subparsers(parser, parser, COMMANDS, options)
|
||||
|
@ -43,7 +43,6 @@ def _discover_system_pip() -> List[str]:
|
||||
|
||||
|
||||
def _run_pip_subprocess(pip_executable: List[str], args: List[str]) -> bytes:
|
||||
import subprocess
|
||||
|
||||
cmd = [*pip_executable, *args]
|
||||
try:
|
||||
|
@ -1,9 +1,11 @@
|
||||
from httpie.manager.tasks.sessions import cli_sessions
|
||||
from httpie.manager.tasks.export_args import cli_export_args
|
||||
from httpie.manager.tasks.plugins import cli_plugins
|
||||
from httpie.manager.tasks.check_updates import cli_check_updates
|
||||
|
||||
CLI_TASKS = {
|
||||
'sessions': cli_sessions,
|
||||
'export-args': cli_export_args,
|
||||
'plugins': cli_plugins,
|
||||
'check-updates': cli_check_updates
|
||||
}
|
||||
|
10
httpie/manager/tasks/check_updates.py
Normal file
10
httpie/manager/tasks/check_updates.py
Normal file
@ -0,0 +1,10 @@
|
||||
import argparse
|
||||
from httpie.context import Environment
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.internal.update_warnings import fetch_updates, get_update_status
|
||||
|
||||
|
||||
def cli_check_updates(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
fetch_updates(env, lazy=False)
|
||||
env.stdout.write(get_update_status(env))
|
||||
return ExitStatus.SUCCESS
|
@ -1,11 +1,11 @@
|
||||
import argparse
|
||||
from typing import Tuple
|
||||
|
||||
from httpie.sessions import SESSIONS_DIR_NAME, get_httpie_session
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.context import Environment
|
||||
from httpie.legacy import v3_1_0_session_cookie_format, v3_2_0_session_header_format
|
||||
from httpie.manager.cli import missing_subcommand, parser
|
||||
from httpie.utils import is_version_greater
|
||||
|
||||
|
||||
FIXERS_TO_VERSIONS = {
|
||||
@ -27,25 +27,6 @@ def cli_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
raise ValueError(f'Unexpected action: {action}')
|
||||
|
||||
|
||||
def is_version_greater(version_1: str, version_2: str) -> bool:
|
||||
# In an ideal scenario, we would depend on `packaging` in order
|
||||
# to offer PEP 440 compatible parsing. But since it might not be
|
||||
# commonly available for outside packages, and since we are only
|
||||
# going to parse HTTPie's own version it should be fine to compare
|
||||
# this in a SemVer subset fashion.
|
||||
|
||||
def split_version(version: str) -> Tuple[int, ...]:
|
||||
parts = []
|
||||
for part in version.split('.')[:3]:
|
||||
try:
|
||||
parts.append(int(part))
|
||||
except ValueError:
|
||||
break
|
||||
return tuple(parts)
|
||||
|
||||
return split_version(version_1) > split_version(version_2)
|
||||
|
||||
|
||||
def upgrade_session(env: Environment, args: argparse.Namespace, hostname: str, session_name: str):
|
||||
session = get_httpie_session(
|
||||
env=env,
|
||||
|
@ -16,7 +16,6 @@ from .cli.constants import (
|
||||
from .compat import cached_property
|
||||
from .utils import split_cookies, parse_content_type_header
|
||||
|
||||
|
||||
ELAPSED_TIME_LABEL = 'Elapsed time'
|
||||
|
||||
|
||||
@ -67,27 +66,10 @@ class HTTPResponse(HTTPMessage):
|
||||
def iter_lines(self, chunk_size):
|
||||
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
@property
|
||||
def headers(self):
|
||||
try:
|
||||
raw = self._orig.raw
|
||||
if getattr(raw, '_original_response', None):
|
||||
raw_version = raw._original_response.version
|
||||
else:
|
||||
raw_version = raw.version
|
||||
except AttributeError:
|
||||
# Assume HTTP/1.1
|
||||
raw_version = 11
|
||||
version = {
|
||||
9: '0.9',
|
||||
10: '1.0',
|
||||
11: '1.1',
|
||||
20: '2.0',
|
||||
}[raw_version]
|
||||
|
||||
original = self._orig
|
||||
status_line = f'HTTP/{version} {original.status_code} {original.reason}'
|
||||
status_line = f'HTTP/{self.version} {original.status_code} {original.reason}'
|
||||
headers = [status_line]
|
||||
headers.extend(
|
||||
': '.join(header)
|
||||
@ -117,6 +99,32 @@ class HTTPResponse(HTTPMessage):
|
||||
for key, value in data.items()
|
||||
)
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""
|
||||
Return the HTTP version used by the server, e.g. '1.1'.
|
||||
|
||||
Assume HTTP/1.1 if version is not available.
|
||||
|
||||
"""
|
||||
mapping = {
|
||||
9: '0.9',
|
||||
10: '1.0',
|
||||
11: '1.1',
|
||||
20: '2.0',
|
||||
}
|
||||
fallback = 11
|
||||
version = None
|
||||
try:
|
||||
raw = self._orig.raw
|
||||
if getattr(raw, '_original_response', None):
|
||||
version = raw._original_response.version
|
||||
else:
|
||||
version = raw.version
|
||||
except AttributeError:
|
||||
pass
|
||||
return mapping[version or fallback]
|
||||
|
||||
|
||||
class HTTPRequest(HTTPMessage):
|
||||
"""A :class:`requests.models.Request` wrapper."""
|
||||
|
@ -7,6 +7,13 @@ from httpie.context import Environment
|
||||
MAN_COMMAND = 'man'
|
||||
NO_MAN_PAGES = os.getenv('HTTPIE_NO_MAN_PAGES', False)
|
||||
|
||||
# On some systems, HTTP(n) might exist but we are only
|
||||
# interested in HTTP(1).
|
||||
#
|
||||
# For more information on man page sections: https://unix.stackexchange.com/a/138643
|
||||
|
||||
MAN_PAGE_SECTION = '1'
|
||||
|
||||
|
||||
def is_available(program: str) -> bool:
|
||||
"""Check whether HTTPie's man pages are available in this system."""
|
||||
@ -14,12 +21,19 @@ def is_available(program: str) -> bool:
|
||||
if NO_MAN_PAGES or os.system == 'nt':
|
||||
return False
|
||||
|
||||
try:
|
||||
process = subprocess.run(
|
||||
[MAN_COMMAND, program],
|
||||
[MAN_COMMAND, MAN_PAGE_SECTION, program],
|
||||
shell=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
except Exception:
|
||||
# There might be some errors outside of the process, e.g
|
||||
# a permission error to execute something that is not an
|
||||
# executable.
|
||||
return False
|
||||
else:
|
||||
return process.returncode == 0
|
||||
|
||||
|
||||
@ -27,5 +41,7 @@ def display_for(env: Environment, program: str) -> None:
|
||||
"""Display the man page for the given command (http/https)."""
|
||||
|
||||
subprocess.run(
|
||||
[MAN_COMMAND, program], stdout=env.stdout, stderr=env.stderr
|
||||
[MAN_COMMAND, MAN_PAGE_SECTION, program],
|
||||
stdout=env.stdout,
|
||||
stderr=env.stderr
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
PYGMENTS_BRIGHT_BLACK = 'ansibrightblack'
|
||||
@ -34,7 +35,21 @@ class ColorString(str):
|
||||
|
||||
E.g: PieColor.BLUE | BOLD | ITALIC
|
||||
"""
|
||||
if isinstance(other, str):
|
||||
# In case of PieColor.BLUE | SOMETHING
|
||||
# we just create a new string.
|
||||
return ColorString(self + ' ' + other)
|
||||
elif isinstance(other, GenericColor):
|
||||
# If we see a GenericColor, then we'll wrap it
|
||||
# in with the desired property in a different class.
|
||||
return _StyledGenericColor(other, styles=self.split())
|
||||
elif isinstance(other, _StyledGenericColor):
|
||||
# And if it is already wrapped, we'll just extend the
|
||||
# list of properties.
|
||||
other.styles.extend(self.split())
|
||||
return other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class PieColor(ColorString, Enum):
|
||||
@ -86,6 +101,12 @@ class GenericColor(Enum):
|
||||
return exposed_color
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StyledGenericColor:
|
||||
color: 'GenericColor'
|
||||
styles: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# noinspection PyDictCreation
|
||||
COLOR_PALETTE = {
|
||||
# Copy the brand palette
|
||||
|
@ -26,6 +26,7 @@ STYLE_BOLD = 'bold'
|
||||
MAX_CHOICE_CHARS = 80
|
||||
|
||||
LEFT_PADDING_2 = (0, 0, 0, 2)
|
||||
LEFT_PADDING_3 = (0, 0, 0, 3)
|
||||
LEFT_PADDING_4 = (0, 0, 0, 4)
|
||||
LEFT_PADDING_5 = (0, 0, 0, 4)
|
||||
|
||||
@ -33,6 +34,12 @@ LEFT_INDENT_2 = (1, 0, 0, 2)
|
||||
LEFT_INDENT_3 = (1, 0, 0, 3)
|
||||
LEFT_INDENT_BOTTOM_3 = (0, 0, 1, 3)
|
||||
|
||||
MORE_INFO_COMMANDS = """
|
||||
To learn more, you can try:
|
||||
-> running 'http --manual'
|
||||
-> visiting our full documentation at https://httpie.io/docs/cli
|
||||
"""
|
||||
|
||||
|
||||
class OptionsHighlighter(RegexHighlighter):
|
||||
highlights = [
|
||||
@ -213,6 +220,10 @@ def to_help_message(
|
||||
Text('More Information', style=STYLE_SWITCH),
|
||||
LEFT_INDENT_2,
|
||||
)
|
||||
yield Padding(
|
||||
MORE_INFO_COMMANDS.rstrip('\n'),
|
||||
LEFT_PADDING_3
|
||||
)
|
||||
yield Padding(
|
||||
spec.epilog.rstrip('\n'),
|
||||
LEFT_INDENT_BOTTOM_3,
|
||||
|
@ -4,20 +4,22 @@ from typing import TYPE_CHECKING, Any, Optional
|
||||
if TYPE_CHECKING:
|
||||
from rich.theme import Theme
|
||||
|
||||
from httpie.output.ui.palette import GenericColor, PieStyle, Styles # noqa
|
||||
from httpie.output.ui.palette import GenericColor, PieStyle, Styles, ColorString, _StyledGenericColor # noqa
|
||||
|
||||
RICH_BOLD = ColorString('bold')
|
||||
|
||||
# Rich-specific color code declarations
|
||||
# <https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py>
|
||||
CUSTOM_STYLES = {
|
||||
'progress.description': GenericColor.WHITE,
|
||||
'progress.data.speed': GenericColor.GREEN,
|
||||
'progress.percentage': GenericColor.AQUA,
|
||||
'progress.download': GenericColor.AQUA,
|
||||
'progress.remaining': GenericColor.ORANGE,
|
||||
'bar.complete': GenericColor.PURPLE,
|
||||
'bar.finished': GenericColor.GREEN,
|
||||
'bar.pulse': GenericColor.PURPLE,
|
||||
'option': GenericColor.PINK,
|
||||
'progress.description': RICH_BOLD | GenericColor.WHITE,
|
||||
'progress.data.speed': RICH_BOLD | GenericColor.GREEN,
|
||||
'progress.percentage': RICH_BOLD | GenericColor.AQUA,
|
||||
'progress.download': RICH_BOLD | GenericColor.AQUA,
|
||||
'progress.remaining': RICH_BOLD | GenericColor.ORANGE,
|
||||
'bar.complete': RICH_BOLD | GenericColor.PURPLE,
|
||||
'bar.finished': RICH_BOLD | GenericColor.GREEN,
|
||||
'bar.pulse': RICH_BOLD | GenericColor.PURPLE,
|
||||
'option': RICH_BOLD | GenericColor.PINK,
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +42,7 @@ class _GenericColorCaster(dict):
|
||||
return super().get(self._translate(key))
|
||||
|
||||
|
||||
def _make_rich_color_theme(style_name: Optional[str]) -> 'Theme':
|
||||
def _make_rich_color_theme(style_name: Optional[str] = None) -> 'Theme':
|
||||
from rich.style import Style
|
||||
from rich.theme import Theme
|
||||
|
||||
@ -55,9 +57,15 @@ def _make_rich_color_theme(style_name: Optional[str]) -> 'Theme':
|
||||
for color, color_set in ChainMap(
|
||||
GenericColor.__members__, CUSTOM_STYLES
|
||||
).items():
|
||||
if isinstance(color_set, _StyledGenericColor):
|
||||
properties = dict.fromkeys(color_set.styles, True)
|
||||
color_set = color_set.color
|
||||
else:
|
||||
properties = {}
|
||||
|
||||
theme.styles[color.lower()] = Style(
|
||||
color=color_set.apply_style(style, style_name=style_name),
|
||||
bold=style is Styles.PIE,
|
||||
**properties,
|
||||
)
|
||||
|
||||
# E.g translate GenericColor.BLUE into blue on key access
|
||||
|
@ -6,13 +6,15 @@ from contextlib import contextmanager
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.highlighter import Highlighter
|
||||
|
||||
from httpie.output.ui.rich_palette import _make_rich_color_theme
|
||||
|
||||
|
||||
def render_as_string(renderable: RenderableType) -> str:
|
||||
"""Render any `rich` object in a fake console and
|
||||
return a *style-less* version of it as a string."""
|
||||
|
||||
with open(os.devnull, 'w') as null_stream:
|
||||
fake_console = Console(file=null_stream, record=True)
|
||||
fake_console = Console(file=null_stream, record=True, theme=_make_rich_color_theme())
|
||||
fake_console.print(renderable)
|
||||
return fake_console.export_text()
|
||||
|
||||
@ -22,7 +24,7 @@ def enable_highlighter(
|
||||
console: Console,
|
||||
highlighter: Highlighter,
|
||||
) -> Iterator[Console]:
|
||||
"""Enable a higlighter temporarily."""
|
||||
"""Enable a highlighter temporarily."""
|
||||
|
||||
original_highlighter = console.highlighter
|
||||
try:
|
||||
|
@ -17,6 +17,7 @@ from .processing import Conversion, Formatting
|
||||
from .streams import (
|
||||
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
||||
)
|
||||
from ..utils import parse_content_type_header
|
||||
|
||||
|
||||
MESSAGE_SEPARATOR = '\n\n'
|
||||
@ -163,7 +164,10 @@ def get_stream_type_and_kwargs(
|
||||
if not is_stream and message_type is HTTPResponse:
|
||||
# If this is a response, then check the headers for determining
|
||||
# auto-streaming.
|
||||
is_stream = headers.get('Content-Type') == 'text/event-stream'
|
||||
raw_content_type_header = headers.get('Content-Type', None)
|
||||
if raw_content_type_header:
|
||||
content_type_header, _ = parse_content_type_header(raw_content_type_header)
|
||||
is_stream = (content_type_header == 'text/event-stream')
|
||||
|
||||
if not env.stdout_isatty and not prettify_groups:
|
||||
stream_class = RawStream
|
||||
|
@ -4,12 +4,11 @@ from typing import NamedTuple, Optional
|
||||
from httpie.adapters import HTTPAdapter
|
||||
# noinspection PyPackageRequirements
|
||||
from urllib3.util.ssl_ import (
|
||||
DEFAULT_CIPHERS, create_urllib3_context,
|
||||
create_urllib3_context,
|
||||
resolve_ssl_version,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_SSL_CIPHERS = DEFAULT_CIPHERS
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||||
'ssl3': 'PROTOCOL_SSLv3',
|
||||
@ -81,6 +80,10 @@ class HTTPieHTTPSAdapter(HTTPAdapter):
|
||||
cert_reqs=ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_default_ciphers_names(cls):
|
||||
return [cipher['name'] for cipher in cls._create_ssl_context(verify=False).get_ciphers()]
|
||||
|
||||
|
||||
def _is_key_file_encrypted(key_file):
|
||||
"""Detects if a key file is encrypted or not.
|
||||
@ -94,3 +97,9 @@ def _is_key_file_encrypted(key_file):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# We used to import the default set of TLS ciphers from urllib3, but they removed it.
|
||||
# Instead, now urllib3 uses the list of ciphers configured by the system.
|
||||
# <https://github.com/httpie/httpie/pull/1501>
|
||||
DEFAULT_SSL_CIPHERS_STRING = ':'.join(HTTPieHTTPSAdapter.get_default_ciphers_names())
|
||||
|
@ -1,16 +1,20 @@
|
||||
import os
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
import sysconfig
|
||||
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from http.cookiejar import parse_ns_headers
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from urllib.parse import urlsplit
|
||||
from typing import Any, List, Optional, Tuple, Callable, Iterable, TypeVar
|
||||
from typing import Any, List, Optional, Tuple, Generator, Callable, Iterable, IO, TypeVar
|
||||
|
||||
import requests.auth
|
||||
|
||||
@ -241,7 +245,7 @@ def get_site_paths(path: Path) -> Iterable[Path]:
|
||||
yield as_site(path)
|
||||
|
||||
|
||||
def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
|
||||
def split_iterable(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
|
||||
left, right = [], []
|
||||
for item in iterable:
|
||||
if key(item):
|
||||
@ -261,3 +265,45 @@ def unwrap_context(exc: Exception) -> Optional[Exception]:
|
||||
|
||||
def url_as_host(url: str) -> str:
|
||||
return urlsplit(url).netloc.split('@')[-1]
|
||||
|
||||
|
||||
class LockFileError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_with_lockfile(file: Path, *args, **kwargs) -> Generator[IO[Any], None, None]:
|
||||
file_id = base64.b64encode(os.fsencode(file)).decode()
|
||||
target_file = Path(tempfile.gettempdir()) / file_id
|
||||
|
||||
# Have an atomic-like touch here, so we'll tighten the possibility of
|
||||
# a race occurring between multiple processes accessing the same file.
|
||||
try:
|
||||
target_file.touch(exist_ok=False)
|
||||
except FileExistsError as exc:
|
||||
raise LockFileError("Can't modify a locked file.") from exc
|
||||
|
||||
try:
|
||||
with open(file, *args, **kwargs) as stream:
|
||||
yield stream
|
||||
finally:
|
||||
target_file.unlink()
|
||||
|
||||
|
||||
def is_version_greater(version_1: str, version_2: str) -> bool:
|
||||
# In an ideal scenario, we would depend on `packaging` in order
|
||||
# to offer PEP 440 compatible parsing. But since it might not be
|
||||
# commonly available for outside packages, and since we are only
|
||||
# going to parse HTTPie's own version it should be fine to compare
|
||||
# this in a SemVer subset fashion.
|
||||
|
||||
def split_version(version: str) -> Tuple[int, ...]:
|
||||
parts = []
|
||||
for part in version.split('.')[:3]:
|
||||
try:
|
||||
parts.append(int(part))
|
||||
except ValueError:
|
||||
break
|
||||
return tuple(parts)
|
||||
|
||||
return split_version(version_1) > split_version(version_2)
|
||||
|
2
setup.py
2
setup.py
@ -13,6 +13,7 @@ tests_require = [
|
||||
'pytest-httpbin>=0.0.6',
|
||||
'pytest-lazy-fixture>=0.0.6',
|
||||
'responses',
|
||||
'pytest-mock',
|
||||
'werkzeug<2.1.0'
|
||||
]
|
||||
dev_require = [
|
||||
@ -116,5 +117,6 @@ setup(
|
||||
data_files=[
|
||||
('share/man/man1', ['extras/man/http.1']),
|
||||
('share/man/man1', ['extras/man/https.1']),
|
||||
('share/man/man1', ['extras/man/httpie.1']),
|
||||
]
|
||||
)
|
||||
|
@ -5,7 +5,7 @@ import responses
|
||||
|
||||
from httpie.cli.constants import PRETTY_MAP
|
||||
from httpie.cli.exceptions import ParseError
|
||||
from httpie.cli.nested_json import HTTPieSyntaxError
|
||||
from httpie.cli.nested_json import NestedJSONSyntaxError
|
||||
from httpie.output.formatters.colors import ColorFormatter
|
||||
from httpie.utils import JsonDictPreservingDuplicateKeys
|
||||
|
||||
@ -157,7 +157,7 @@ def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
|
||||
f'option:={json.dumps(value)}',
|
||||
)
|
||||
|
||||
cm.match("Can't use complex JSON value types")
|
||||
cm.match('Cannot use complex JSON value types')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -508,23 +508,23 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
|
||||
),
|
||||
(
|
||||
['foo=1', 'foo[key]:=2'],
|
||||
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
|
||||
"HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
|
||||
),
|
||||
(
|
||||
['foo=1', 'foo[0]:=2'],
|
||||
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
|
||||
"HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
|
||||
),
|
||||
(
|
||||
['foo=1', 'foo[]:=2'],
|
||||
"HTTPie Type Error: Can't perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
|
||||
"HTTPie Type Error: Cannot perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
|
||||
),
|
||||
(
|
||||
['data[key]=value', 'data[key 2]=value 2', 'data[0]=value'],
|
||||
"HTTPie Type Error: Can't perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
|
||||
"HTTPie Type Error: Cannot perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
|
||||
),
|
||||
(
|
||||
['data[key]=value', 'data[key 2]=value 2', 'data[]=value'],
|
||||
"HTTPie Type Error: Can't perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
|
||||
"HTTPie Type Error: Cannot perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
|
||||
),
|
||||
(
|
||||
[
|
||||
@ -532,7 +532,7 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
|
||||
'foo[bar][baz][5][]:=4',
|
||||
'foo[bar][baz][key][]:=5',
|
||||
],
|
||||
"HTTPie Type Error: Can't perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
|
||||
"HTTPie Type Error: Cannot perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
|
||||
),
|
||||
(
|
||||
['foo[-10]:=[1,2]'],
|
||||
@ -540,32 +540,32 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
|
||||
),
|
||||
(
|
||||
['foo[0]:=1', 'foo[]:=2', 'foo[\\2]:=3'],
|
||||
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^",
|
||||
"HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^",
|
||||
),
|
||||
(
|
||||
['foo[\\1]:=2', 'foo[5]:=3'],
|
||||
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^",
|
||||
"HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^",
|
||||
),
|
||||
(
|
||||
['x=y', '[]:=2'],
|
||||
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
|
||||
"HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
|
||||
),
|
||||
(
|
||||
['[]:=2', 'x=y'],
|
||||
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
|
||||
"HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
|
||||
),
|
||||
(
|
||||
[':=[1,2,3]', '[]:=4'],
|
||||
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
|
||||
"HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
|
||||
),
|
||||
(
|
||||
['[]:=4', ':=[1,2,3]'],
|
||||
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
|
||||
"HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_nested_json_errors(input_json, expected_error, httpbin):
|
||||
with pytest.raises(HTTPieSyntaxError) as exc:
|
||||
with pytest.raises(NestedJSONSyntaxError) as exc:
|
||||
http(httpbin + '/post', *input_json)
|
||||
|
||||
exc_lines = str(exc.value).splitlines()
|
||||
|
@ -117,6 +117,8 @@ def test_plugins_double_uninstall(httpie_plugins, httpie_plugins_success, dummy_
|
||||
)
|
||||
|
||||
|
||||
# TODO: Make this work on CI (stopped working at some point)
|
||||
@pytest.mark.skip(reason='Doesn’t work in CI')
|
||||
@pytest.mark.requires_installation
|
||||
def test_plugins_upgrade(httpie_plugins, httpie_plugins_success, dummy_plugin):
|
||||
httpie_plugins_success("install", dummy_plugin.path)
|
||||
|
@ -446,7 +446,7 @@ class TestExpiredCookies(CookieTestBase):
|
||||
class TestCookieStorage(CookieTestBase):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'new_cookies, new_cookies_dict, expected',
|
||||
['specified_cookie_header', 'new_cookies_dict', 'expected_effective_cookie_header'],
|
||||
[(
|
||||
'new=bar',
|
||||
{'new': 'bar'},
|
||||
@ -463,9 +463,9 @@ class TestCookieStorage(CookieTestBase):
|
||||
'chocolate=milk; cookie1=foo; cookie2=foo; new=bar'
|
||||
),
|
||||
(
|
||||
'new=bar;; chocolate=milk;;;',
|
||||
'new=bar; chocolate=milk',
|
||||
{'new': 'bar', 'chocolate': 'milk'},
|
||||
'cookie1=foo; cookie2=foo; new=bar'
|
||||
'cookie1=foo; cookie2=foo; new=bar; chocolate=milk'
|
||||
),
|
||||
(
|
||||
'new=bar; chocolate=milk;;;',
|
||||
@ -474,20 +474,35 @@ class TestCookieStorage(CookieTestBase):
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_existing_and_new_cookies_sent_in_request(self, new_cookies, new_cookies_dict, expected, httpbin):
|
||||
def test_existing_and_new_cookies_sent_in_request(
|
||||
self,
|
||||
specified_cookie_header,
|
||||
new_cookies_dict,
|
||||
expected_effective_cookie_header,
|
||||
httpbin,
|
||||
):
|
||||
r = http(
|
||||
'--session', str(self.session_path),
|
||||
'--print=H',
|
||||
httpbin.url,
|
||||
'Cookie:' + new_cookies,
|
||||
'Cookie:' + specified_cookie_header,
|
||||
)
|
||||
# Note: cookies in response are in alphabetical order
|
||||
assert f'Cookie: {expected}' in r
|
||||
parsed_request_headers = { # noqa
|
||||
name: value for name, value in [
|
||||
line.split(': ', 1)
|
||||
for line in r.splitlines()
|
||||
if line and ':' in line
|
||||
]
|
||||
}
|
||||
# Note: cookies in the request are in an undefined order.
|
||||
expected_request_cookie_set = set(expected_effective_cookie_header.split('; '))
|
||||
actual_request_cookie_set = set(parsed_request_headers['Cookie'].split('; '))
|
||||
assert actual_request_cookie_set == expected_request_cookie_set
|
||||
|
||||
updated_session = json.loads(self.session_path.read_text(encoding=UTF8))
|
||||
for name, value in new_cookies_dict.items():
|
||||
assert name, value in updated_session['cookies']
|
||||
assert 'Cookie' not in updated_session['headers']
|
||||
for name, value in new_cookies_dict.items():
|
||||
assert updated_session['cookies'][name]['value'] == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'cli_cookie, set_cookie, expected',
|
||||
|
@ -7,7 +7,7 @@ import urllib3
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_STRING
|
||||
from httpie.status import ExitStatus
|
||||
|
||||
from .utils import HTTP_OK, TESTS_ROOT, IS_PYOPENSSL, http
|
||||
@ -146,7 +146,7 @@ def test_ciphers(httpbin_secure):
|
||||
r = http(
|
||||
httpbin_secure.url + '/get',
|
||||
'--ciphers',
|
||||
DEFAULT_SSL_CIPHERS,
|
||||
DEFAULT_SSL_CIPHERS_STRING,
|
||||
)
|
||||
assert HTTP_OK in r
|
||||
|
||||
|
@ -124,6 +124,10 @@ def test_redirected_stream(httpbin):
|
||||
['Accept:text/event-stream'],
|
||||
3
|
||||
),
|
||||
(
|
||||
['Accept:text/event-stream; charset=utf-8'],
|
||||
3
|
||||
),
|
||||
(
|
||||
['Accept:text/plain'],
|
||||
1
|
||||
|
237
tests/test_update_warnings.py
Normal file
237
tests/test_update_warnings.py
Normal file
@ -0,0 +1,237 @@
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from httpie.internal.daemon_runner import STATUS_FILE
|
||||
from httpie.internal.daemons import spawn_daemon
|
||||
from httpie.status import ExitStatus
|
||||
|
||||
from .utils import PersistentMockEnvironment, http, httpie
|
||||
|
||||
BUILD_CHANNEL = 'test'
|
||||
BUILD_CHANNEL_2 = 'test2'
|
||||
UNKNOWN_BUILD_CHANNEL = 'test3'
|
||||
|
||||
HIGHEST_VERSION = '999.999.999'
|
||||
LOWEST_VERSION = '1.1.1'
|
||||
|
||||
FIXED_DATE = datetime(1970, 1, 1).isoformat()
|
||||
|
||||
MAX_ATTEMPT = 40
|
||||
MAX_TIMEOUT = 2.0
|
||||
|
||||
|
||||
def check_update_warnings(text):
|
||||
return 'A new HTTPie release' in text
|
||||
|
||||
|
||||
@pytest.mark.requires_external_processes
|
||||
def test_daemon_runner():
|
||||
# We have a pseudo daemon task called 'check_status'
|
||||
# which creates a temp file called STATUS_FILE under
|
||||
# user's temp directory. This test simply ensures that
|
||||
# we create a daemon that successfully performs the
|
||||
# external task.
|
||||
|
||||
status_file = Path(tempfile.gettempdir()) / STATUS_FILE
|
||||
with suppress(FileNotFoundError):
|
||||
status_file.unlink()
|
||||
|
||||
spawn_daemon('check_status')
|
||||
|
||||
for attempt in range(MAX_ATTEMPT):
|
||||
time.sleep(MAX_TIMEOUT / MAX_ATTEMPT)
|
||||
if status_file.exists():
|
||||
break
|
||||
else:
|
||||
pytest.fail(
|
||||
'Maximum number of attempts failed for daemon status check.'
|
||||
)
|
||||
|
||||
assert status_file.exists()
|
||||
|
||||
|
||||
def test_fetch(static_fetch_data, without_warnings):
|
||||
http('fetch_updates', '--daemon', env=without_warnings)
|
||||
|
||||
with open(without_warnings.config.version_info_file) as stream:
|
||||
version_data = json.load(stream)
|
||||
|
||||
assert version_data['last_warned_date'] is None
|
||||
assert version_data['last_fetched_date'] is not None
|
||||
assert (
|
||||
version_data['last_released_versions'][BUILD_CHANNEL]
|
||||
== HIGHEST_VERSION
|
||||
)
|
||||
assert (
|
||||
version_data['last_released_versions'][BUILD_CHANNEL_2]
|
||||
== LOWEST_VERSION
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_dont_override_existing_layout(
|
||||
static_fetch_data, without_warnings
|
||||
):
|
||||
with open(without_warnings.config.version_info_file, 'w') as stream:
|
||||
existing_layout = {
|
||||
'last_warned_date': FIXED_DATE,
|
||||
'last_fetched_date': FIXED_DATE,
|
||||
'last_released_versions': {BUILD_CHANNEL: LOWEST_VERSION},
|
||||
}
|
||||
json.dump(existing_layout, stream)
|
||||
|
||||
http('fetch_updates', '--daemon', env=without_warnings)
|
||||
|
||||
with open(without_warnings.config.version_info_file) as stream:
|
||||
version_data = json.load(stream)
|
||||
|
||||
# The "last updated at" field should not be modified, but the
|
||||
# rest need to be updated.
|
||||
assert version_data['last_warned_date'] == FIXED_DATE
|
||||
assert version_data['last_fetched_date'] != FIXED_DATE
|
||||
assert (
|
||||
version_data['last_released_versions'][BUILD_CHANNEL]
|
||||
== HIGHEST_VERSION
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_broken_json(static_fetch_data, without_warnings):
|
||||
with open(without_warnings.config.version_info_file, 'w') as stream:
|
||||
stream.write('$$broken$$')
|
||||
|
||||
http('fetch_updates', '--daemon', env=without_warnings)
|
||||
|
||||
with open(without_warnings.config.version_info_file) as stream:
|
||||
version_data = json.load(stream)
|
||||
|
||||
assert (
|
||||
version_data['last_released_versions'][BUILD_CHANNEL]
|
||||
== HIGHEST_VERSION
|
||||
)
|
||||
|
||||
|
||||
def test_check_updates_disable_warnings(
|
||||
without_warnings, httpbin, fetch_update_mock
|
||||
):
|
||||
r = http(httpbin + '/get', env=without_warnings)
|
||||
assert not fetch_update_mock.called
|
||||
assert not check_update_warnings(r.stderr)
|
||||
|
||||
|
||||
def test_check_updates_first_invocation(
|
||||
with_warnings, httpbin, fetch_update_mock
|
||||
):
|
||||
r = http(httpbin + '/get', env=with_warnings)
|
||||
assert fetch_update_mock.called
|
||||
assert not check_update_warnings(r.stderr)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'should_issue_warning, build_channel',
|
||||
[
|
||||
(False, pytest.lazy_fixture('lower_build_channel')),
|
||||
(True, pytest.lazy_fixture('higher_build_channel')),
|
||||
],
|
||||
)
|
||||
def test_check_updates_first_time_after_data_fetch(
|
||||
with_warnings,
|
||||
httpbin,
|
||||
fetch_update_mock,
|
||||
static_fetch_data,
|
||||
should_issue_warning,
|
||||
build_channel,
|
||||
):
|
||||
http('fetch_updates', '--daemon', env=with_warnings)
|
||||
r = http(httpbin + '/get', env=with_warnings)
|
||||
|
||||
assert not fetch_update_mock.called
|
||||
assert (not should_issue_warning) or check_update_warnings(r.stderr)
|
||||
|
||||
|
||||
def test_check_updates_first_time_after_data_fetch_unknown_build_channel(
|
||||
with_warnings,
|
||||
httpbin,
|
||||
fetch_update_mock,
|
||||
static_fetch_data,
|
||||
unknown_build_channel,
|
||||
):
|
||||
http('fetch_updates', '--daemon', env=with_warnings)
|
||||
r = http(httpbin + '/get', env=with_warnings)
|
||||
|
||||
assert not fetch_update_mock.called
|
||||
assert not check_update_warnings(r.stderr)
|
||||
|
||||
|
||||
def test_cli_check_updates(
|
||||
static_fetch_data, higher_build_channel
|
||||
):
|
||||
r = httpie('cli', 'check-updates')
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
assert check_update_warnings(r)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"build_channel", [
|
||||
pytest.lazy_fixture("lower_build_channel"),
|
||||
pytest.lazy_fixture("unknown_build_channel")
|
||||
]
|
||||
)
|
||||
def test_cli_check_updates_not_shown(
|
||||
static_fetch_data, build_channel
|
||||
):
|
||||
r = httpie('cli', 'check-updates')
|
||||
assert r.exit_status == ExitStatus.SUCCESS
|
||||
assert not check_update_warnings(r)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def with_warnings(tmp_path):
|
||||
env = PersistentMockEnvironment()
|
||||
env.config['version_info_file'] = tmp_path / 'version.json'
|
||||
env.config['disable_update_warnings'] = False
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def without_warnings(tmp_path):
|
||||
env = PersistentMockEnvironment()
|
||||
env.config['version_info_file'] = tmp_path / 'version.json'
|
||||
env.config['disable_update_warnings'] = True
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fetch_update_mock(mocker):
|
||||
mock_fetch = mocker.patch('httpie.internal.update_warnings.fetch_updates')
|
||||
return mock_fetch
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def static_fetch_data(mocker):
|
||||
mock_get = mocker.patch('requests.get')
|
||||
mock_get.return_value.status_code = 200
|
||||
mock_get.return_value.json.return_value = {
|
||||
BUILD_CHANNEL: HIGHEST_VERSION,
|
||||
BUILD_CHANNEL_2: LOWEST_VERSION,
|
||||
}
|
||||
return mock_get
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unknown_build_channel(mocker):
|
||||
mocker.patch('httpie.internal.update_warnings.BUILD_CHANNEL', UNKNOWN_BUILD_CHANNEL)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def higher_build_channel(mocker):
|
||||
mocker.patch('httpie.internal.update_warnings.BUILD_CHANNEL', BUILD_CHANNEL)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lower_build_channel(mocker):
|
||||
mocker.patch('httpie.internal.update_warnings.BUILD_CHANNEL', BUILD_CHANNEL_2)
|
@ -18,7 +18,7 @@ from .utils import (
|
||||
)
|
||||
from .fixtures import FILE_PATH_ARG, FILE_PATH, FILE_CONTENT
|
||||
|
||||
MAX_RESPONSE_WAIT_TIME = 2
|
||||
MAX_RESPONSE_WAIT_TIME = 5
|
||||
|
||||
|
||||
def test_chunked_json(httpbin_with_chunked_support):
|
||||
|
@ -49,6 +49,10 @@ HTTP_OK_COLOR = (
|
||||
DUMMY_URL = 'http://this-should.never-resolve' # Note: URL never fetched
|
||||
DUMMY_HOST = url_as_host(DUMMY_URL)
|
||||
|
||||
# We don't want hundreds of subprocesses trying to access GitHub API
|
||||
# during the tests.
|
||||
Config.DEFAULTS['disable_update_warnings'] = True
|
||||
|
||||
|
||||
def strip_colors(colorized_msg: str) -> str:
|
||||
return COLOR_RE.sub('', colorized_msg)
|
||||
@ -163,6 +167,7 @@ class MockEnvironment(Environment):
|
||||
self._delete_config_dir = True
|
||||
|
||||
def cleanup(self):
|
||||
self.devnull.close()
|
||||
self.stdout.close()
|
||||
self.stderr.close()
|
||||
warnings.resetwarnings()
|
||||
@ -179,6 +184,11 @@ class MockEnvironment(Environment):
|
||||
pass
|
||||
|
||||
|
||||
class PersistentMockEnvironment(MockEnvironment):
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
|
||||
class BaseCLIResponse:
|
||||
"""
|
||||
Represents the result of simulated `$ http' invocation via `http()`.
|
||||
@ -442,7 +452,4 @@ def http(
|
||||
return r
|
||||
|
||||
finally:
|
||||
devnull.close()
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
env.cleanup()
|
||||
|
Reference in New Issue
Block a user