forked from extern/httpie-cli
Compare commits
131 Commits
3.0.0
...
fix-initia
Author | SHA1 | Date | |
---|---|---|---|
9d2e2afede | |||
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 | |||
f7c1bb269e | |||
0f9fd76852 | |||
af1d6b1853 | |||
419cc2c34a | |||
79a8ecd84b | |||
d262181bed | |||
732878f1b4 | |||
83803db14d | |||
dd2c9513f3 | |||
278dfc487d | |||
ff6f1887b0 | |||
86f4bf4d0a | |||
e6d0bfec7c | |||
9f1ec6d5cc | |||
85ba9ad8ea | |||
d03e3f4e14 | |||
c157948531 | |||
33ea977b64 | |||
d1596dde12 | |||
af2ffb6999 | |||
0632c4d614 | |||
6787a2bd29 | |||
9d2864b966 | |||
a5288f0cd6 | |||
8efa7cb04d | |||
baec1b2202 | |||
266c6375c6 | |||
77af4c7a5c | |||
7509dd4e6c | |||
f08c1bee17 | |||
59d9e928f8 | |||
0a873172c9 | |||
614866eeb2 | |||
395914fb4d | |||
65ab7d5caa | |||
b5623ccc87 | |||
ec203b1fac | |||
350abe3033 | |||
9241a09360 | |||
15013fd609 | |||
98688b2f2d | |||
5ac05e9514 | |||
5c98253377 | |||
b0f5b8ab26 | |||
55087a901e | |||
c901e70463 | |||
25bd817bb2 | |||
6f77e144e4 | |||
6bf39e469f | |||
30cd862fc0 | |||
ad613f29d2 | |||
225dccb218 | |||
cafa11665b | |||
0a9d3d3c54 | |||
e306667436 | |||
384d3869f6 | |||
5fd48e3137 | |||
37ef670876 | |||
46e782bf75 | |||
42edb1eb76 | |||
d45f413f12 | |||
f1ea486025 | |||
7abddfe350 | |||
86ba995ad8 | |||
c03f081a7e | |||
a7d8187b21 | |||
fc383e9b78 | |||
770df02291 | |||
f756cad58d | |||
fde64d578d | |||
c8404493e5 | |||
559134de0a | |||
813e8864a1 | |||
45fcd746d7 | |||
d5e3611e85 | |||
378a1f513e | |||
df6843b15a | |||
640901146f | |||
6b5d96da72 | |||
97bd9c2a89 | |||
708608e1d4 | |||
d56a1f216e | |||
738a6bea57 | |||
ec521c461b | |||
212000199e | |||
700dbeddb0 | |||
30a4d29f77 | |||
aedcad7e2a | |||
202f59e04a | |||
ba0c1ab258 | |||
217cf8ddae | |||
859e442083 | |||
4e59bbfae6 | |||
caa8fb9058 | |||
2797b7244c | |||
3b441fa57e | |||
c815e21ef9 | |||
8a03b7a824 | |||
b3f29c8d1e | |||
a88e44c284 | |||
c97fe64a37 |
28
.github/workflows/autogenerated-files.yml
vendored
Normal file
28
.github/workflows/autogenerated-files.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Update Autogenerated Files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
regen-autogenerated-files:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- run: make regen-all
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: "[automated] Update auto-generated files"
|
||||
title: "[automated] Update auto-generated files"
|
||||
delete-branch: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
8
.github/workflows/benchmark.yml
vendored
8
.github/workflows/benchmark.yml
vendored
@ -13,8 +13,8 @@ jobs:
|
||||
if: github.event.label.name == 'benchmark'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
@ -30,7 +30,7 @@ jobs:
|
||||
echo "::set-output name=body::$body"
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@ -38,7 +38,7 @@ jobs:
|
||||
body-includes: '# Benchmarks'
|
||||
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
6
.github/workflows/code-style.yml
vendored
6
.github/workflows/code-style.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Code Style Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@ -11,8 +13,8 @@ jobs:
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make venv
|
||||
|
6
.github/workflows/coverage.yml
vendored
6
.github/workflows/coverage.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Coverage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@ -10,8 +12,8 @@ jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- run: make install
|
||||
|
4
.github/workflows/docs-check-markdown.yml
vendored
4
.github/workflows/docs-check-markdown.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Check Markdown Style
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@ -8,7 +10,7 @@ jobs:
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
|
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
31
.github/workflows/docs-update-install.yml
vendored
31
.github/workflows/docs-update-install.yml
vendored
@ -1,31 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/docs-update-install.yml
|
||||
- docs/installation/*
|
||||
|
||||
# Allow to call the workflow manually
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make install
|
||||
- run: make doc-update-install
|
||||
- uses: Automattic/action-commit-to-branch@master
|
||||
with:
|
||||
branch: master
|
||||
commit_message: |
|
||||
Auto-update install docs
|
||||
|
||||
Via .github/workflows/docs-update-install.yml
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
26
.github/workflows/release-brew.yml
vendored
Normal file
26
.github/workflows/release-brew.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Release on Homebrew
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
brew-release:
|
||||
name: Release the Homebrew Package
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: mislav/bump-homebrew-formula-action@v1
|
||||
with:
|
||||
formula-name: httpie
|
||||
tag-name: ${{ github.events.inputs.branch }}
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.BREW_UPDATE_TOKEN }}
|
61
.github/workflows/release-choco.yml
vendored
Normal file
61
.github/workflows/release-choco.yml
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
name: Release on Chocolatey
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
brew-release:
|
||||
name: Release the Chocolatey
|
||||
runs-on: windows-2019
|
||||
env:
|
||||
package-dir: docs\packaging\windows-chocolatey
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
# Chocolatey comes already installed on the Windows GHA image
|
||||
- name: Build the Choco package
|
||||
shell: cmd
|
||||
run: choco pack -v
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
||||
- name: Check the Choco package
|
||||
run: choco info httpie -s .
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
||||
- 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
|
||||
choco uninstall -y httpie
|
||||
working-directory: ${{ env.package-dir }}
|
||||
|
||||
- name: Publish on Chocolatey
|
||||
shell: bash
|
||||
env:
|
||||
CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }}
|
||||
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 }}
|
77
.github/workflows/release-linux-standalone.yml
vendored
Normal file
77
.github/workflows/release-linux-standalone.yml
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
name: Release as Standalone Linux Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
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]
|
||||
|
||||
|
||||
jobs:
|
||||
binary-build-and-release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Build Artifacts
|
||||
run: |
|
||||
cd extras/packaging/linux
|
||||
./get_release_artifacts.sh
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: http
|
||||
path: extras/packaging/linux/artifacts/dist/http
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: httpie.deb
|
||||
path: extras/packaging/linux/artifacts/dist/*.deb
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
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
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
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
|
30
.github/workflows/release-pypi.yml
vendored
Normal file
30
.github/workflows/release-pypi.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Release on PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
pypi-build-and-release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: make install && make build
|
||||
|
||||
- name: Release on PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
24
.github/workflows/release-snap.yml
vendored
24
.github/workflows/release-snap.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Release on Snap
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@ -7,16 +9,32 @@ on:
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
snap:
|
||||
snap-build-and-release:
|
||||
name: Build & Release the Snap Package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
# If any of the stages fail, then we'll stop the action
|
||||
# to give release manager time to investigate the underlying
|
||||
# issue.
|
||||
fail-fast: true
|
||||
matrix:
|
||||
level: [edge, beta, candidate, stable]
|
||||
|
||||
# Set the concurrency level for this version, so
|
||||
# that we'll release one by one.
|
||||
concurrency: ${{ github.event.inputs.branch }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- uses: snapcore/action-build@v1
|
||||
id: build
|
||||
|
||||
- uses: snapcore/action-publish@v1
|
||||
with:
|
||||
store_login: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: edge
|
||||
release: ${{ matrix.level }}
|
||||
|
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@ -1,31 +0,0 @@
|
||||
on:
|
||||
# Add a "Trigger" button to manually start the workflow.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
# It could be fully automated by uncommenting following lines.
|
||||
# Let's see later if we are confident enough to try it :)
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
jobs:
|
||||
new-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
- name: PyPi configuration
|
||||
run: |
|
||||
echo "[distutils]\nindex-servers=\n httpie\n\n[httpie]\nrepository = https://upload.pypi.org/legacy/\n" > $HOME/.pypirc
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
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@v4
|
||||
- uses: actions/stale@v5
|
||||
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'
|
||||
|
@ -1,3 +1,6 @@
|
||||
name: Test Snap Package (Linux)
|
||||
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@ -9,7 +12,7 @@ jobs:
|
||||
snap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build
|
||||
uses: snapcore/action-build@v1
|
||||
id: snapcraft
|
||||
|
4
.github/workflows/test-package-mac-brew.yml
vendored
4
.github/workflows/test-package-mac-brew.yml
vendored
@ -1,3 +1,5 @@
|
||||
name: Test Brew Package (MacOS)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@ -9,7 +11,7 @@ jobs:
|
||||
brew:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup brew
|
||||
run: |
|
||||
brew developer on
|
||||
|
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@ -1,3 +1,8 @@
|
||||
name: Tests
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -24,8 +29,8 @@ jobs:
|
||||
pyopenssl: [0, 1]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Windows setup
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -43,8 +43,8 @@ MANIFEST
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
*.manifest
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@ -151,3 +151,5 @@ dmypy.json
|
||||
|
||||
# Windows Chocolatey
|
||||
*.nupkg
|
||||
|
||||
artifacts/
|
||||
|
12
.packit.yaml
12
.packit.yaml
@ -3,16 +3,10 @@
|
||||
specfile_path: httpie.spec
|
||||
actions:
|
||||
# get the current Fedora Rawhide specfile:
|
||||
# post-upstream-clone: "wget https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -O httpie.spec"
|
||||
post-upstream-clone: "cp docs/packaging/linux-fedora/httpie.spec.txt httpie.spec"
|
||||
post-upstream-clone: "wget https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -O httpie.spec"
|
||||
# Use this when the latest spec is not up-to-date.
|
||||
# post-upstream-clone: "cp docs/packaging/linux-fedora/httpie.spec.txt httpie.spec"
|
||||
jobs:
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-all
|
||||
additional_repos:
|
||||
- "https://kojipkgs.fedoraproject.org/repos/f$releasever-build/latest/$basearch/"
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
metadata:
|
||||
|
49
CHANGELOG.md
49
CHANGELOG.md
@ -3,8 +3,53 @@
|
||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [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))
|
||||
- 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))
|
||||
- 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)
|
||||
|
||||
- **SECURITY** Fixed the [vulnerability](https://github.com/httpie/httpie/security/advisories/GHSA-9w4w-cpc8-h2fq) that caused exposure of cookies on redirects to third party hosts. ([#1312](https://github.com/httpie/httpie/pull/1312))
|
||||
- Fixed escaping of integer indexes with multiple backslashes in the nested JSON builder. ([#1285](https://github.com/httpie/httpie/issues/1285))
|
||||
- Fixed displaying of status code without a status message on non-`auto` themes. ([#1300](https://github.com/httpie/httpie/issues/1300))
|
||||
- Fixed redundant issuance of stdin detection warnings on some rare cases due to underlying implementation. ([#1303](https://github.com/httpie/httpie/pull/1303))
|
||||
- Fixed double `--quiet` so that it will now suppress all python level warnings. ([#1271](https://github.com/httpie/httpie/issues/1271))
|
||||
- Added support for specifying certificate private key passphrases through `--cert-key-pass` and prompts. ([#946](https://github.com/httpie/httpie/issues/946))
|
||||
- Added `httpie cli export-args` command for exposing the parser specification for the `http`/`https` commands. ([#1293](https://github.com/httpie/httpie/pull/1293))
|
||||
- Improved regulation of top-level arrays. ([#1292](https://github.com/httpie/httpie/commit/225dccb2186f14f871695b6c4e0bfbcdb2e3aa28))
|
||||
- Improved UI layout for standalone invocations. ([#1296](https://github.com/httpie/httpie/pull/1296))
|
||||
|
||||
## [3.0.2](https://github.com/httpie/httpie/compare/3.0.1...3.0.2) (2022-01-24)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Fixed usage of `httpie` when there is a presence of a config with `default_options`. ([#1280](https://github.com/httpie/httpie/pull/1280))
|
||||
|
||||
## [3.0.1](https://github.com/httpie/httpie/compare/3.0.0...3.0.1) (2022-01-23)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Changed the value shown as time elapsed from time-to-read-headers to total exchange time. ([#1277](https://github.com/httpie/httpie/issues/1277))
|
||||
|
||||
## [3.0.0](https://github.com/httpie/httpie/compare/2.6.0...3.0.0) (2022-01-21)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Dropped support for Python 3.6. ([#1177](https://github.com/httpie/httpie/issues/1177))
|
||||
- Improved startup time by 40%. ([#1211](https://github.com/httpie/httpie/pull/1211))
|
||||
- Added support for nested JSON syntax. ([#1169](https://github.com/httpie/httpie/issues/1169))
|
||||
@ -28,7 +73,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [2.6.0](https://github.com/httpie/httpie/compare/2.5.0...2.6.0) (2021-10-14)
|
||||
|
||||
[What’s new in HTTPie 2.6.0 →](https://httpie.io/blog/httpie-2.6.0)
|
||||
[What’s new in HTTPie for Terminal 2.6.0 →](https://httpie.io/blog/httpie-2.6.0)
|
||||
|
||||
- Added support for formatting & coloring of JSON bodies preceded by non-JSON data (e.g., an XXSI prefix). ([#1130](https://github.com/httpie/httpie/issues/1130))
|
||||
- Added charset auto-detection when `Content-Type` doesn’t include it. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
@ -40,7 +85,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [2.5.0](https://github.com/httpie/httpie/compare/2.4.0...2.5.0) (2021-09-06)
|
||||
|
||||
[What’s new in HTTPie 2.5.0 →](https://httpie.io/blog/httpie-2.5.0)
|
||||
[What’s new in HTTPie for Terminal 2.5.0 →](https://httpie.io/blog/httpie-2.5.0)
|
||||
|
||||
- Added `--raw` to allow specifying the raw request body without extra processing as
|
||||
an alternative to `stdin`. ([#534](https://github.com/httpie/httpie/issues/534))
|
||||
|
@ -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
|
||||
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright © 2012-2021 Jakub Roztocil <jakub@roztocil.co>
|
||||
Copyright © 2012-2022 Jakub Roztocil <jakub@roztocil.co>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
55
Makefile
55
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 | egrep -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 .
|
||||
@ -147,19 +167,17 @@ doc-check:
|
||||
mdl --git-recurse --style docs/markdownlint.rb .
|
||||
|
||||
|
||||
doc-update-install:
|
||||
@echo $(H1)Updating installation instructions in the docs$(H1END)
|
||||
$(VENV_PYTHON) docs/installation/generate.py
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Publishing to PyPi
|
||||
###############################################################################
|
||||
|
||||
|
||||
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
|
||||
@ -203,10 +221,25 @@ 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)
|
||||
brew test httpie
|
||||
http --version
|
||||
https --version
|
||||
|
||||
@echo $(H1)Auditing…$(H1END)
|
||||
brew audit --strict httpie
|
||||
|
||||
###############################################################################
|
||||
# Regeneration
|
||||
###############################################################################
|
||||
|
||||
regen-all: regen-man-pages regen-install-methods
|
||||
|
||||
regen-man-pages: install
|
||||
@echo $(H1)Regenerate man pages$(H1END)
|
||||
$(VENV_PYTHON) extras/scripts/generate_man_pages.py
|
||||
|
||||
regen-install-methods:
|
||||
@echo $(H1)Updating installation instructions in the docs$(H1END)
|
||||
$(VENV_PYTHON) docs/installation/generate.py
|
||||
|
@ -21,6 +21,14 @@ They use simple and natural syntax and provide formatted and colorized output.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
|
||||
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Installation instructions →](https://httpie.io/docs#installation)
|
||||
|
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
When you identify a vulnerability in HTTPie, please report it privately using one of the following channels:
|
||||
|
||||
- Email to [`security@httpie.io`](mailto:security@httpie.io)
|
||||
- Report on [huntr.dev](https://huntr.dev/)
|
||||
|
||||
In addition to the description of the vulnerability, include the following information:
|
||||
|
||||
- A short reproducer to verify it (it can be a small HTTP server, shell script, docker image, etc.)
|
||||
- Your deemed severity level of the vulnerability (`LOW`/`MEDIUM`/`HIGH`/`CRITICAL`)
|
||||
- [CWE](https://cwe.mitre.org/) ID, if available.
|
497
docs/README.md
497
docs/README.md
@ -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
|
||||
```
|
||||
@ -205,12 +207,27 @@ Also works for other Arch-derived distributions like ArcoLinux, EndeavourOS, Art
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ pacman -Sy httpie
|
||||
$ pacman -Syu httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ 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
|
||||
@ -260,7 +277,7 @@ Verify that now you have the [current development version identifier](https://gi
|
||||
|
||||
```bash
|
||||
$ http --version
|
||||
# 3.0.0
|
||||
# 3.X.X.dev0
|
||||
```
|
||||
|
||||
## Usage
|
||||
@ -277,7 +294,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
|
||||
|
||||
@ -448,7 +465,7 @@ $ http https://api.github.com/search/repositories q==httpie per_page==1
|
||||
GET /search/repositories?q=httpie&per_page=1 HTTP/1.1
|
||||
```
|
||||
|
||||
You can even retrieve the `value` from a file by using the `param==@file` syntax. This would also effectively strip the newlines from the end. See [#file-based-separators] for more examples.
|
||||
You can even retrieve the `value` from a file by using the `param==@file` syntax. This would also effectively strip the newlines from the end. See [file based separators](#file-based-separators) for more examples.
|
||||
|
||||
```bash
|
||||
$ http pie.dev/get text==@files/text.txt
|
||||
@ -538,7 +555,7 @@ and URL parameters. This is a very practical way of constructing
|
||||
HTTP requests from scratch on the CLI.
|
||||
|
||||
Each *request item* is simply a key/value pair separated with the following
|
||||
characters: `:` (headers), `=` (data field, e.g JSON, Form), `:=` (raw data field)
|
||||
characters: `:` (headers), `=` (data field, e.g., JSON, form), `:=` (raw data field)
|
||||
`==` (query parameters), `@` (file upload).
|
||||
|
||||
```bash
|
||||
@ -550,9 +567,9 @@ $ http PUT pie.dev/put \
|
||||
```
|
||||
|
||||
| Item Type | Description |
|
||||
| -----------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|-------------------------------------------------------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| HTTP Headers `Name:Value` | Arbitrary HTTP header, e.g. `X-API-Token:123` |
|
||||
| URL parameters `name==value` | Appends the given name/value pair as a querystring parameter to the URL. The `==` separator is used. |
|
||||
| URL parameters `name==value` | Appends the given name/value pair as a querystring parameter to the URL. The `==` separator is used. |
|
||||
| Data Fields `field=value` | Request data fields to be serialized as a JSON object (default), to be form-encoded (with `--form, -f`), or to be serialized as `multipart/form-data` (with `--multipart`) |
|
||||
| Raw JSON fields `field:=json` | Useful when sending JSON and one or more fields need to be a `Boolean`, `Number`, nested `Object`, or an `Array`, e.g., `meals:='["ham","spam"]'` or `pies:=[1,2,3]` (note the quotes) |
|
||||
| File upload fields `field@/dir/file`, `field@file;type=mime` | Only available with `--form`, `-f` and `--multipart`. For example `screenshot@~/Pictures/img.png`, or `'cv@cv.txt;type=text/markdown'`. With `--form`, the presence of a file field results in a `--multipart` request |
|
||||
@ -570,7 +587,7 @@ to pass the desired value from a file.
|
||||
$ http POST pie.dev/post \
|
||||
X-Data:@files/text.txt # Read a header from a file
|
||||
token==@files/text.txt # Read a query parameter from a file
|
||||
name=@files/text.txt # Read a data field's value from a file
|
||||
name=@files/text.txt # Read a data field’s value from a file
|
||||
bookmarks:=@files/data.json # Embed a JSON object from a file
|
||||
```
|
||||
|
||||
@ -681,11 +698,44 @@ Other JSON types, however, are not allowed with `--form` or `--multipart`.
|
||||
"about": {
|
||||
"mission": "Make APIs simple and intuitive",
|
||||
"homepage": "httpie.io",
|
||||
}
|
||||
"stars": 54000
|
||||
},
|
||||
"apps": [
|
||||
"Terminal",
|
||||
"Desktop",
|
||||
"Web",
|
||||
"Mobile"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Introduction
|
||||
|
||||
Let’s start with a simple example, and build a simple search query:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=id \
|
||||
search[id]:=1
|
||||
```
|
||||
|
||||
In the example above, the `search[type]` is an instruction for creating an object called `search`, and setting the `type` field of it to the given value (`"id"`).
|
||||
|
||||
Also note that, just as the regular syntax, you can use the `:=` operator to directly pass raw JSON values (e.g, numbers in the case above).
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"id": 1,
|
||||
"type": "id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Building arrays is also possible, through `[]` suffix (an append operation). This tells HTTPie to create an array in the given path (if there is not one already), and append the given value to that array.
|
||||
Building arrays is also possible, through `[]` suffix (an append operation). This tells HTTPie to create an array in the given path (if there is not one already), and append the given value to that array.
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
@ -696,7 +746,7 @@ $ http --offline --print=B pie.dev/post \
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"keywords": [
|
||||
@ -739,7 +789,7 @@ $ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=platforms \
|
||||
search[platforms][]=Terminal \
|
||||
search[platforms][1]=Desktop \
|
||||
search[platforms][1]=Desktop \
|
||||
search[platforms][3]=Mobile
|
||||
```
|
||||
|
||||
@ -807,7 +857,7 @@ $ http --offline --print=B pie.dev/post \
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
[]:=1 \
|
||||
[]:=2 \
|
||||
'foo\[bar\]:=1' \
|
||||
[]:=3
|
||||
```
|
||||
|
||||
```json
|
||||
@ -821,6 +871,47 @@ $ http PUT pie.dev/put \
|
||||
You can also apply the nesting to the items by referencing their index:
|
||||
|
||||
```bash
|
||||
http --offline --print=B pie.dev/post \
|
||||
[0][type]=platform [0][name]=terminal \
|
||||
[1][type]=platform [1][name]=desktop
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "terminal"
|
||||
},
|
||||
{
|
||||
"type": "platform",
|
||||
"name": "desktop"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
##### Escaping behavior
|
||||
|
||||
Nested JSON syntax uses the same [escaping rules](#escaping-rules) as
|
||||
the terminal. There are 3 special characters, and 1 special token that you can escape.
|
||||
|
||||
If you want to send a bracket as is, escape it with a backslash (`\`):
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'foo\[bar\]:=1' \
|
||||
'baz[\[]:=2' \
|
||||
'baz[\]]:=3'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"baz": {
|
||||
"[": 2,
|
||||
"]": 3
|
||||
},
|
||||
"foo[bar]": 1
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send the literal backslash character (`\`), escape it with another backslash:
|
||||
|
||||
@ -845,7 +936,7 @@ $ http --offline --print=B pie.dev/post \
|
||||
'object[\1]=stringified' \
|
||||
'object[\100]=same' \
|
||||
'array[1]=indexified'
|
||||
```
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
@ -903,8 +994,8 @@ You can follow to given instruction (adding a `]`) and repair your expression.
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
{
|
||||
"user": {
|
||||
"name": "string"
|
||||
}
|
||||
}
|
||||
@ -1010,7 +1101,7 @@ world
|
||||
|
||||
```bash
|
||||
$ http --form --multipart --offline example.org hello=world Content-Type:multipart/letter
|
||||
```
|
||||
```
|
||||
|
||||
```http
|
||||
POST / HTTP/1.1
|
||||
@ -1104,7 +1195,7 @@ To send a header with an empty value, use `Header;`, with a semicolon:
|
||||
|
||||
```bash
|
||||
http --offline example.org Numbers:one,two
|
||||
```
|
||||
```
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
@ -1174,7 +1265,7 @@ $ nc pie.dev 80 < request.http
|
||||
Cookie: sessionid=foo
|
||||
Host: pie.dev
|
||||
User-Agent: HTTPie/0.9.9
|
||||
```
|
||||
```
|
||||
|
||||
Send multiple cookies (note: the header is quoted to prevent the shell from interpreting the `;`):
|
||||
|
||||
@ -1247,12 +1338,18 @@ https -A bearer -a token pie.dev/bearer
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ cat ~/.netrc
|
||||
machine pie.dev
|
||||
login httpie
|
||||
password test
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http pie.dev/basic-auth/httpie/test
|
||||
HTTP/1.1 200 OK
|
||||
[...]
|
||||
```
|
||||
|
||||
This can be disabled with the `--ignore-netrc` option:
|
||||
|
||||
```bash
|
||||
@ -1297,9 +1394,11 @@ Here are a few picks:
|
||||
$ http --follow pie.dev/redirect/3
|
||||
```
|
||||
|
||||
|
||||
With `307 Temporary Redirect` and `308 Permanent Redirect`, the method and the body of the original request
|
||||
are reused to perform the redirected request. Otherwise, a body-less `GET` request is performed.
|
||||
|
||||
### Showing intermediary redirect responses
|
||||
|
||||
If you wish to see the intermediary requests/responses,
|
||||
then use the `--all` option:
|
||||
|
||||
@ -1407,6 +1506,21 @@ path of the key file with `--cert-key`:
|
||||
(The actually available set of protocols may vary depending on your OpenSSL installation.)
|
||||
|
||||
```bash
|
||||
# Specify the vulnerable SSL v3 protocol to talk to an outdated server:
|
||||
$ http --ssl=ssl3 https://vulnerable.example.org
|
||||
```
|
||||
|
||||
### SSL ciphers
|
||||
|
||||
You can specify the available ciphers with `--ciphers`.
|
||||
It should be a string in the [OpenSSL cipher list format](https://www.openssl.org/docs/man1.1.0/man1/ciphers.html).
|
||||
|
||||
```bash
|
||||
$ http --ciphers=ECDHE-RSA-AES128-GCM-SHA256 https://pie.dev/get
|
||||
```
|
||||
|
||||
Note: these cipher strings do not change the negotiated version of SSL or TLS, they only affect the list of available cipher suites.
|
||||
|
||||
To see the default cipher string, run `http --help` and see the `--ciphers` section under SSL.
|
||||
|
||||
## Output options
|
||||
@ -1438,28 +1552,28 @@ By default, HTTPie only outputs the final response and the whole response
|
||||
| `b` | response body |
|
||||
| `m` | [response meta](#response-meta) |
|
||||
|
||||
Print request and response headers:
|
||||
|
||||
```bash
|
||||
$ http --print=Hh PUT pie.dev/put hello=world
|
||||
```
|
||||
|
||||
Print request and response headers:
|
||||
|
||||
```bash
|
||||
$ http --print=Hh PUT pie.dev/put hello=world
|
||||
```
|
||||
|
||||
#### Response meta
|
||||
|
||||
`--verbose` can often be useful for debugging the request and generating documentation examples:
|
||||
|
||||
The response metadata section currently includes the total time elapsed. It’s the number of seconds between opening the network connection and downloading the last byte of response the body.
|
||||
|
||||
|
||||
To _only_ show the response metadata, use `--meta, -m` (analogically to `--headers, -h` and `--body, -b`):
|
||||
|
||||
```bash
|
||||
$ http --meta pie.dev/delay/1
|
||||
Content-Type: application/json
|
||||
Host: pie.dev
|
||||
User-Agent: HTTPie/0.2.7dev
|
||||
|
||||
{
|
||||
"hello": "world"
|
||||
}
|
||||
```
|
||||
|
||||
```console
|
||||
Elapsed time: 1.099171542s
|
||||
```
|
||||
|
||||
The [extra verbose `-vv` output](#extra-verbose-output) includes the meta section by default. You can also show it in combination with other parts of the exchange via [`--print=m`](#what-parts-of-the-http-exchange-should-be-printed). For example, here we print it together with the response headers:
|
||||
|
||||
```bash
|
||||
$ http --print=hm pie.dev/get
|
||||
@ -1467,6 +1581,40 @@ Print request and response headers:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
|
||||
Please note that it also includes time spent on formatting the output, which adds a small penalty. Also, if the body is not part of the output, [we don’t spend time downloading it](#conditional-body-download).
|
||||
|
||||
If you [use `--style` with one of the Pie themes](#colors-and-formatting), you’ll see the time information color-coded (green/yellow/orange/red) based on how long the exchange took.
|
||||
|
||||
|
||||
### Verbose output
|
||||
|
||||
`--verbose` can often be useful for debugging the request and generating documentation examples:
|
||||
|
||||
```bash
|
||||
$ http --verbose PUT pie.dev/put hello=world
|
||||
PUT /put HTTP/1.1
|
||||
Accept: application/json, */*;q=0.5
|
||||
Accept-Encoding: gzip, deflate
|
||||
Content-Type: application/json
|
||||
Host: pie.dev
|
||||
User-Agent: HTTPie/0.2.7dev
|
||||
|
||||
{
|
||||
"hello": "world"
|
||||
}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Connection: keep-alive
|
||||
Content-Length: 477
|
||||
Content-Type: application/json
|
||||
Date: Sun, 05 Aug 2012 00:25:23 GMT
|
||||
Server: gunicorn/0.13.4
|
||||
|
||||
{
|
||||
[…]
|
||||
}
|
||||
@ -1496,9 +1644,9 @@ Server: gunicorn/0.13.4
|
||||
```bash
|
||||
# There will be no output, even in case of an unexpected response status code:
|
||||
$ http -qq --check-status pie.dev/post enjoy='the silence without warnings'
|
||||
$ http -qq --check-status pie.dev/post enjoy='the silence without warnings'
|
||||
```
|
||||
|
||||
|
||||
### Update warnings
|
||||
|
||||
When there is a new release available for your platform (for example; if you installed HTTPie through `pip`, it will check the latest version on `PyPI`), HTTPie will regularly warn you about the new update (once a week). If you want to disable this behavior, you can set `disable_update_warnings` to `true` in your [config](#config) file.
|
||||
|
||||
@ -1522,6 +1670,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
|
||||
|
||||
@ -1534,14 +1686,6 @@ $ http --all --follow pie.dev/redirect/3
|
||||
`--raw='data'`, and `@/file/path`.
|
||||
|
||||
### Redirected Input
|
||||
## Raw request body
|
||||
|
||||
In addition to crafting structured [JSON](#json) and [forms](#forms) requests with the [request items](#request-items) syntax, you can provide a raw request body that will be sent without further processing.
|
||||
These two approaches for specifying request data (i.e., structured and raw) cannot be combined.
|
||||
|
||||
There are three methods for passing raw request data: piping via `stdin`,
|
||||
`--raw='data'`, and `@/file/path`.
|
||||
|
||||
|
||||
The universal method for passing request data is through redirected `stdin`
|
||||
(standard input)—piping.
|
||||
@ -1628,7 +1772,7 @@ On macOS, you can send the contents of the clipboard with `pbpaste`:
|
||||
|
||||
### Request data from a filename
|
||||
|
||||
|
||||
An alternative to redirected `stdin` is specifying a filename (as `@/path/to/file`) whose content is used as if it came from `stdin`.
|
||||
|
||||
It has the advantage that the `Content-Type` header is automatically set to the appropriate value based on the filename extension.
|
||||
For example, the following request sends the verbatim contents of that XML file with `Content-Type: application/xml`:
|
||||
@ -1711,21 +1855,21 @@ Syntax highlighting is applied to HTTP headers and bodies (where it makes sense)
|
||||
|
||||
```bash
|
||||
$ http --response-mime=text/yaml pie.dev/get
|
||||
$ http --response-mime=text/yaml pie.dev/get
|
||||
```
|
||||
|
||||
Formatting has the following effects:
|
||||
|
||||
- HTTP headers are sorted by name.
|
||||
- JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||
to the characters they represent.
|
||||
- XML and XHTML data is indented.
|
||||
|
||||
```
|
||||
|
||||
Formatting has the following effects:
|
||||
|
||||
- HTTP headers are sorted by name.
|
||||
- JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||
to the characters they represent.
|
||||
- XML and XHTML data is indented.
|
||||
|
||||
Please note that sometimes there might be changes made by formatters on the actual response body (e.g.,
|
||||
collapsing empty tags on XML) but the end result will always be semantically indistinguishable. Some of
|
||||
these formatting changes can be configured more granularly through [format options](#format-options).
|
||||
|
||||
### Format options
|
||||
### Format options
|
||||
|
||||
The `--format-options=opt1:value,opt2:value` option allows you to control how the output should be formatted
|
||||
when formatting is applied. The following options are available:
|
||||
|
||||
@ -1744,7 +1888,7 @@ Formatting has the following effects:
|
||||
```bash
|
||||
$ http --format-options headers.sort:false,json.sort_keys:false,json.indent:2 pie.dev/get
|
||||
```
|
||||
```
|
||||
|
||||
There are also two shortcuts that allow you to quickly disable and re-enable
|
||||
sorting-related format options (currently it means JSON keys and headers):
|
||||
`--unsorted` and `--sorted`.
|
||||
@ -1754,7 +1898,7 @@ The `--format-options=opt1:value,opt2:value` option allows you to control how th
|
||||
### Redirected output
|
||||
|
||||
HTTPie uses a different set of defaults for redirected output than for [terminal output](#terminal-output).
|
||||
HTTPie uses a different set of defaults for redirected output than for [terminal output](#terminal-output).
|
||||
The differences being:
|
||||
|
||||
- Formatting and colors aren’t applied (unless `--pretty` is specified).
|
||||
- Only the response body is printed (unless one of the [output options](#output-options) is set).
|
||||
@ -1903,7 +2047,7 @@ $ http -dco file.zip example.org/file
|
||||
|
||||
Streamed output by small chunks à la `tail -f`:
|
||||
|
||||
# Send each new line (JSON object) to another URL as soon as it arrives from a streaming API:
|
||||
```bash
|
||||
# Send each new line (JSON object) to another URL as soon as it arrives from a streaming API:
|
||||
$ http --stream pie.dev/stream/3 | while read line; do echo "$line" | http pie.dev/post ; done
|
||||
```
|
||||
@ -1918,6 +2062,8 @@ You can use the `--stream, -S` flag to make two things happen:
|
||||
```bash
|
||||
# Create a new session:
|
||||
$ http --session=./session.json pie.dev/headers API-Token:123
|
||||
```
|
||||
|
||||
```bash
|
||||
# Inspect / edit the generated session file:
|
||||
$ cat session.json
|
||||
@ -1982,7 +2128,7 @@ $ http --session=user2 -a user2:password pie.dev/get X-Bar:Foo
|
||||
```
|
||||
|
||||
When creating anonymous sessions, please remember to always include at least one `/`, even if the session files is located in the current directory (i.e. `--session=./session.json` instead of just `--session=session.json`), otherwise HTTPie assumes a named session instead.
|
||||
When creating anonymous sessions, please remember to always include at least one `/`, even if the session files is located in the current directory (i.e. `--session=./session.json` instead of just `--session=session.json`), otherwise HTTPie assumes a named session instead.
|
||||
|
||||
### Readonly session
|
||||
|
||||
To use the original session file without updating it from the request/response exchange after it has been created, specify the session name via `--session-read-only=SESSION_NAME_OR_PATH` instead.
|
||||
@ -2024,38 +2170,88 @@ $ http --session-read-only=./ro-session.json pie.dev/headers Custom-Header:orig-
|
||||
$ http --session=./session.json pie.dev/cookies
|
||||
```
|
||||
|
||||
},
|
||||
```json
|
||||
{
|
||||
"password": null,
|
||||
"cookies": {
|
||||
"pie": "apple"
|
||||
"username": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To make a cookie domain _unbound_ (i.e., to make it available to all hosts, including throughout a cross-domain redirect chain), you can set the `domain` field to `null` in the session file:
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"domain": null,
|
||||
"name": "unbound-cookie",
|
||||
"value": "send-me-to-any-host"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
"cookies": {
|
||||
|
||||
```bash
|
||||
"expires": null,
|
||||
"path": "/",
|
||||
"secure": false,
|
||||
$ http --session=./session.json pie.dev/cookies
|
||||
```
|
||||
|
||||
```json
|
||||
}
|
||||
{
|
||||
"cookies": {
|
||||
"unbound-cookie": "send-me-to-any-host"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
### Cookie storage behavior
|
||||
|
||||
For example, a cookie set through the command line will overwrite a cookie of the same name stored in the session file.
|
||||
There are three possible sources of persisted cookies within a session. They have the following storage priority: 1—response; 2—command line; 3—session file.
|
||||
|
||||
1. Receive a response with a `Set-Cookie` header:
|
||||
|
||||
```bash
|
||||
$ http --session=./session.json pie.dev/cookie/set?foo=bar
|
||||
```
|
||||
|
||||
2. Send a cookie specified on the command line as seen in [cookies](#cookies):
|
||||
|
||||
```bash
|
||||
$ http --session=./session.json pie.dev/headers Cookie:foo=bar
|
||||
|
||||
Expired cookies are never stored.
|
||||
If a cookie in a session file expires, it will be removed before sending a new request.
|
||||
If the server expires an existing cookie, it will also be removed from the session file.
|
||||
|
||||
## Config
|
||||
|
||||
HTTPie uses a simple `config.json` file.
|
||||
The file doesn’t exist by default but you can create it manually.
|
||||
|
||||
### Config file directory
|
||||
|
||||
```
|
||||
|
||||
3. Manually set cookie parameters in the session file:
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": {
|
||||
"foo": {
|
||||
"expires": null,
|
||||
"path": "/",
|
||||
"secure": false,
|
||||
"value": "bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In summary:
|
||||
|
||||
- Cookies set via the CLI overwrite cookies of the same name inside session files.
|
||||
- Server-sent `Set-Cookie` header cookies overwrite any pre-existing ones with the same name.
|
||||
|
||||
Cookie expiration handling:
|
||||
|
||||
- When the server expires an existing cookie, HTTPie removes it from the session file.
|
||||
- When a cookie in a session file expires, HTTPie removes it before sending a new request.
|
||||
|
||||
### Upgrading sessions
|
||||
|
||||
HTTPie may introduce changes in the session file format. When HTTPie detects an obsolete format, it shows a warning. You can upgrade your session files using the following commands:
|
||||
|
||||
Upgrade all existing [named sessions](#named-sessions) inside the `sessions` subfolder of your [config directory](https://httpie.io/docs/cli/config-file-directory):
|
||||
|
||||
```bash
|
||||
$ httpie cli sessions upgrade-all
|
||||
Upgraded 'api_auth' @ 'pie.dev' to v3.1.0
|
||||
@ -2064,21 +2260,60 @@ To set a cookie within a Session there are three options:
|
||||
|
||||
Upgrading individual sessions requires you to specify the session's hostname. That allows HTTPie to find the correct file in the case of name sessions. Additionally, it allows it to correctly bind cookies when upgrading with [`--bind-cookies`](#session-upgrade-options).
|
||||
|
||||
The config directory can be changed by setting the `$HTTPIE_CONFIG_DIR` environment variable:
|
||||
|
||||
Upgrade a single [named session](#named-sessions):
|
||||
|
||||
```bash
|
||||
$ export HTTPIE_CONFIG_DIR=/tmp/httpie
|
||||
$ http pie.dev/get
|
||||
```
|
||||
$ httpie cli sessions upgrade pie.dev api_auth
|
||||
Upgraded 'api_auth' @ 'pie.dev' to v3.1.0
|
||||
```
|
||||
|
||||
Upgrade a single [anonymous session](#anonymous-sessions) using a file path:
|
||||
|
||||
```bash
|
||||
$ httpie cli sessions upgrade pie.dev ./session.json
|
||||
Upgraded 'session.json' @ 'pie.dev' to v3.1.0
|
||||
```
|
||||
|
||||
#### Session upgrade options
|
||||
|
||||
These flags are available for both `sessions upgrade` and `sessions upgrade-all`:
|
||||
|
||||
| Option | Description |
|
||||
|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--bind-cookies` | Bind all previously [unbound cookies](#host-based-cookie-policy) to the session’s host ([context](https://github.com/httpie/httpie/security/advisories/GHSA-9w4w-cpc8-h2fq)). |
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
HTTPie uses a simple `config.json` file.
|
||||
The file doesn’t exist by default, but you can create it manually.
|
||||
|
||||
### Config file directory
|
||||
|
||||
To see the exact location for your installation, run `http --debug` and look for `config_dir` in the output.
|
||||
|
||||
The default location of the configuration file on most platforms is `$XDG_CONFIG_HOME/httpie/config.json` (defaulting to `~/.config/httpie/config.json`).
|
||||
|
||||
For backward compatibility, if the directory `~/.httpie` exists, the configuration file there will be used instead.
|
||||
|
||||
On Windows, the config file is located at `%APPDATA%\httpie\config.json`.
|
||||
|
||||
The config directory can be changed by setting the `$HTTPIE_CONFIG_DIR` environment variable:
|
||||
|
||||
```bash
|
||||
$ export HTTPIE_CONFIG_DIR=/tmp/httpie
|
||||
$ http pie.dev/get
|
||||
```
|
||||
|
||||
### Configurable options
|
||||
|
||||
Currently, HTTPie offers a single configurable option:
|
||||
|
||||
### Configurable options
|
||||
|
||||
Currently, HTTPie offers a single configurable option:
|
||||
#### `default_options`
|
||||
|
||||
An `Array` (by default empty) of default options that should be applied to every invocation of HTTPie.
|
||||
|
||||
|
||||
For instance, you can use this config option to change your default color theme:
|
||||
|
||||
```bash
|
||||
$ cat ~/.config/httpie/config.json
|
||||
@ -2086,7 +2321,7 @@ To see the exact location for your installation, run `http --debug` and look for
|
||||
|
||||
```json
|
||||
{
|
||||
{
|
||||
"default_options": [
|
||||
"--style=fruity"
|
||||
]
|
||||
}
|
||||
@ -2120,12 +2355,12 @@ $ cat ~/.config/httpie/config.json
|
||||
2) echo 'Request timed out!' ;;
|
||||
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
|
||||
4) echo 'HTTP 4xx Client Error!' ;;
|
||||
4) echo 'HTTP 4xx Client Error!' ;;
|
||||
5) echo 'HTTP 5xx Server Error!' ;;
|
||||
6) echo 'Exceeded --max-redirects=<n> redirects!' ;;
|
||||
*) echo 'Other Error!' ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
### Best practices
|
||||
|
||||
@ -2166,9 +2401,10 @@ And since there’s neither data nor `EOF`, it will get stuck. So unless you’r
|
||||
```
|
||||
|
||||
#### `httpie cli export-args`
|
||||
#### `httpie plugins install`
|
||||
|
||||
`httpie cli export-args` command can expose the parser specification of `http`/`https` commands
|
||||
For installing plugins from [PyPI](https://pypi.org/) or from local paths, `httpie plugins install`
|
||||
(like an API definition) to outside tools so that they can use this to build better interactions
|
||||
over them (e.g., offer auto-complete).
|
||||
|
||||
Available formats to export in include:
|
||||
|
||||
@ -2181,24 +2417,53 @@ For managing these plugins; starting with 3.0, we are offering a new plugin mana
|
||||
```bash
|
||||
$ httpie cli export-args | jq '"Program: " + .spec.name + ", Version: " + .version'
|
||||
"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.
|
||||
|
||||
##### `httpie cli plugins install`
|
||||
|
||||
For installing plugins from [PyPI](https://pypi.org/) or from local paths, `httpie cli plugins install`
|
||||
can be used.
|
||||
|
||||
```bash
|
||||
$ httpie cli plugins install httpie-plugin
|
||||
Installing httpie-plugin...
|
||||
Successfully installed httpie-plugin-1.0.2
|
||||
```
|
||||
|
||||
> Tip: Generally HTTPie plugins start with `httpie-` prefix. Try searching for it on [PyPI](https://pypi.org/search/?q=httpie-)
|
||||
> to find out all plugins from the community.
|
||||
|
||||
##### `httpie cli plugins list`
|
||||
|
||||
List all installed plugins.
|
||||
|
||||
```bash
|
||||
$ httpie cli plugins list
|
||||
$ httpie plugins list
|
||||
httpie_plugin (1.0.2)
|
||||
httpie_plugin (1.0.2)
|
||||
httpie_plugin (httpie.plugins.auth.v1)
|
||||
httpie_plugin_2 (1.0.6)
|
||||
httpie_plugin_2 (httpie.plugins.auth.v1)
|
||||
httpie_plugin_2 (httpie.plugins.auth.v1)
|
||||
httpie_converter (1.0.0)
|
||||
httpie_iterm_converter (httpie.plugins.converter.v1)
|
||||
httpie_konsole_konverter (httpie.plugins.converter.v1)
|
||||
httpie_konsole_konverter (httpie.plugins.converter.v1)
|
||||
```
|
||||
|
||||
|
||||
##### `httpie cli plugins upgrade`
|
||||
|
||||
For upgrading already installed plugins, use `httpie plugins upgrade`.
|
||||
|
||||
|
||||
```bash
|
||||
$ httpie cli plugins upgrade httpie-plugin
|
||||
```
|
||||
|
||||
@ -2206,12 +2471,12 @@ Successfully installed httpie-plugin-1.0.2
|
||||
|
||||
Uninstall plugins from the isolated plugins directory. If the plugin is not installed
|
||||
through `httpie cli plugins install`, it won’t uninstall it.
|
||||
through `httpie plugins install`, it won't uninstall it.
|
||||
|
||||
```bash
|
||||
$ httpie cli plugins uninstall httpie-plugin
|
||||
```
|
||||
|
||||
|
||||
## Meta
|
||||
|
||||
### Interface design
|
||||
|
||||
@ -2221,21 +2486,21 @@ httpie_converter (1.0.0)
|
||||
For example, compare this HTTP request:
|
||||
|
||||
```http
|
||||
```http
|
||||
POST /post HTTP/1.1
|
||||
Host: pie.dev
|
||||
X-API-Key: 123
|
||||
User-Agent: Bacon/1.0
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
name=value&name2=value2
|
||||
```
|
||||
```
|
||||
|
||||
with the HTTPie command that sends it:
|
||||
|
||||
|
||||
```bash
|
||||
$ http -f POST pie.dev/post \
|
||||
X-API-Key:123 \
|
||||
X-API-Key:123 \
|
||||
User-Agent:Bacon/1.0 \
|
||||
name=value \
|
||||
name2=value2
|
||||
```
|
||||
@ -2311,6 +2576,10 @@ Helpers to convert from other client tools:
|
||||
|
||||
See [CONTRIBUTING](https://github.com/httpie/httpie/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Security policy
|
||||
|
||||
See [github.com/httpie/httpie/security/policy](https://github.com/httpie/httpie/security/policy).
|
||||
|
||||
### Change log
|
||||
|
||||
See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md).
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"website": {
|
||||
"master_and_released_docs_differ_after": "d40f06687f8cbbd22bf7dba05bee93aea11a169f"
|
||||
"master_and_released_docs_differ_after": null
|
||||
}
|
||||
}
|
||||
|
@ -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'])
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,45 @@
|
||||
{
|
||||
"Aaron Miller": {
|
||||
"committed": [],
|
||||
"github": "aaronhmiller",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "aaronmiller8"
|
||||
},
|
||||
"Alexander Bogdanov": {
|
||||
"committed": [],
|
||||
"github": "ab-kily",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Almad": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "Almad",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "almadcz"
|
||||
},
|
||||
"Andr\u00e1s Czig\u00e1ny": {
|
||||
"committed": [],
|
||||
"github": "andrascz",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Annette Wilson": {
|
||||
"committed": [],
|
||||
"github": "annettejanewilson",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -25,6 +51,34 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Batuhan Taskaya": {
|
||||
"committed": [
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "isidentical",
|
||||
"reported": [
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": "isidentical"
|
||||
},
|
||||
"Brad Crittenden": {
|
||||
"committed": [],
|
||||
"github": "bac",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Chad": {
|
||||
"committed": [],
|
||||
"github": "cythrawll",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"D8ger": {
|
||||
"committed": [],
|
||||
"github": "caofanCPU",
|
||||
@ -35,7 +89,8 @@
|
||||
},
|
||||
"Dave": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "davecheney",
|
||||
"reported": [],
|
||||
@ -49,6 +104,14 @@
|
||||
],
|
||||
"twitter": "DawidFerenczy"
|
||||
},
|
||||
"Ed Rooth": {
|
||||
"committed": [],
|
||||
"github": "sym3tri",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Elena Lape": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -57,11 +120,20 @@
|
||||
"reported": [],
|
||||
"twitter": "elena_lape"
|
||||
},
|
||||
"Ethan Mills": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "ethanmills",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Fabio Peruzzo": {
|
||||
"committed": [],
|
||||
"github": "peruzzof",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -73,6 +145,22 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Gabriel Cruz": {
|
||||
"committed": [],
|
||||
"github": "gmelodie",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "gmelodiecruz"
|
||||
},
|
||||
"Gaurav": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "gkcs",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Giampaolo Rodola": {
|
||||
"committed": [],
|
||||
"github": "giampaolo",
|
||||
@ -81,6 +169,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Greg Myers": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "myersg86",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Hugh Williams": {
|
||||
"committed": [],
|
||||
"github": "hughpv",
|
||||
@ -102,21 +198,35 @@
|
||||
"Jakub Roztocil": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "jakubroztocil",
|
||||
"reported": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "jakubroztocil"
|
||||
},
|
||||
"Jan Bra\u0161na": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "janbrasna",
|
||||
"reported": [],
|
||||
"twitter": "janbrasna"
|
||||
},
|
||||
"Jan Verbeek": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "blyxxyz",
|
||||
"reported": [],
|
||||
"reported": [
|
||||
"3.0.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Jannik Vieten": {
|
||||
@ -127,6 +237,22 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Jesper Holmberg": {
|
||||
"committed": [],
|
||||
"github": "strindberg",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Kirill Krasnov": {
|
||||
"committed": [],
|
||||
"github": "Kirill",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Marcel St\u00f6r": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -135,6 +261,14 @@
|
||||
"reported": [],
|
||||
"twitter": "frightanic"
|
||||
},
|
||||
"Marco Seguri": {
|
||||
"committed": [],
|
||||
"github": "seguri",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Mariano Ruiz": {
|
||||
"committed": [],
|
||||
"github": "mrsarm",
|
||||
@ -143,22 +277,41 @@
|
||||
],
|
||||
"twitter": "mrsarm82"
|
||||
},
|
||||
"Mark Rosenbaum": {
|
||||
"committed": [],
|
||||
"github": "markrosenbaum",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Micka\u00ebl Schoentgen": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "BoboTiG",
|
||||
"reported": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "__tiger222__"
|
||||
},
|
||||
"Mike DePalatis": {
|
||||
"committed": [],
|
||||
"github": "mivade",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Miro Hron\u010dok": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "hroncok",
|
||||
"reported": [],
|
||||
@ -168,20 +321,63 @@
|
||||
"committed": [],
|
||||
"github": "ducaale",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Nanashi.": {
|
||||
"committed": [],
|
||||
"github": "sevenc-nanashi",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"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"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "om26er",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "om26er"
|
||||
},
|
||||
"Patrick Taylor": {
|
||||
"committed": [],
|
||||
"github": "pmeister",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Paul Laffitte": {
|
||||
"committed": [],
|
||||
"github": "paullaffitte",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "plaffitt"
|
||||
},
|
||||
"Pavel Alexeev aka Pahan-Hubbitus": {
|
||||
"committed": [],
|
||||
"github": "Hubbitus",
|
||||
@ -190,6 +386,22 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Roberto L\u00f3pez L\u00f3pez": {
|
||||
"committed": [],
|
||||
"github": "robertolopezlopez",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Russell Shurts": {
|
||||
"committed": [],
|
||||
"github": "rshurts",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Samuel Marks": {
|
||||
"committed": [],
|
||||
"github": "SamuelMarks",
|
||||
@ -198,6 +410,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Sebastian Czech": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "sebastianczech",
|
||||
"reported": [],
|
||||
"twitter": "sebaczech"
|
||||
},
|
||||
"Sullivan SENECHAL": {
|
||||
"committed": [],
|
||||
"github": "soullivaneuh",
|
||||
@ -218,7 +438,32 @@
|
||||
"committed": [],
|
||||
"github": "vovtz",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Vivaan Verma": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "doublevcodes",
|
||||
"reported": [],
|
||||
"twitter": "doublevcodes"
|
||||
},
|
||||
"Vladimir Berkutov": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "dair-targ",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Will Rogers": {
|
||||
"committed": [],
|
||||
"github": "wjrogers",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -238,6 +483,14 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"arloan": {
|
||||
"committed": [],
|
||||
"github": "arloan",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"bl-ue": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -246,22 +499,56 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"blueray453": {
|
||||
"committed": [],
|
||||
"github": "blueray453",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"claudiatd": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "claudiatd",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"coldcoff": {
|
||||
"committed": [],
|
||||
"github": "coldcoff",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"dependabot[bot]": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "dependabot-sr",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"dkreeft": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "dkreeft",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"greg": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "gregkh",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"henryhu712": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -270,14 +557,31 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"hosseingt": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "hosseingt",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"jakubroztocil": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "jkbr",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"josephworks": {
|
||||
"committed": [],
|
||||
"github": "josephworks",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"jungle-boogie": {
|
||||
"committed": [],
|
||||
"github": "jungle-boogie",
|
||||
@ -286,6 +590,22 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"luisuimi": {
|
||||
"committed": [],
|
||||
"github": "luisuimi",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"luzpaz": {
|
||||
"committed": [
|
||||
"3.2.0"
|
||||
],
|
||||
"github": "luzpaz",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"nixbytes": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -294,6 +614,14 @@
|
||||
"reported": [],
|
||||
"twitter": "linuxbyte3"
|
||||
},
|
||||
"peterpt": {
|
||||
"committed": [],
|
||||
"github": "peterpt",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"qiulang": {
|
||||
"committed": [],
|
||||
"github": "qiulang",
|
||||
@ -302,6 +630,38 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"stonebig": {
|
||||
"committed": [],
|
||||
"github": "stonebig",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"whodidthis": {
|
||||
"committed": [],
|
||||
"github": "whodidthis",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zhaohanqing95": {
|
||||
"committed": [],
|
||||
"github": "zhaohanqing95",
|
||||
"reported": [
|
||||
"3.2.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zoulja": {
|
||||
"committed": [],
|
||||
"github": "zoulja",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zwx00": {
|
||||
"committed": [],
|
||||
"github": "zwx00",
|
||||
@ -314,7 +674,8 @@
|
||||
"committed": [],
|
||||
"github": "rogerdehe",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
@ -322,8 +683,9 @@
|
||||
"committed": [],
|
||||
"github": "hh-in-zhuzhou",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 -->
|
||||
|
||||
|
@ -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:
|
||||
@ -106,9 +109,9 @@ tools:
|
||||
package: https://archlinux.org/packages/community/any/httpie/
|
||||
commands:
|
||||
install:
|
||||
- pacman -Sy httpie
|
||||
upgrade:
|
||||
- pacman -Syu httpie
|
||||
upgrade:
|
||||
- pacman -Syu
|
||||
|
||||
pkg:
|
||||
title: FreshPorts
|
||||
@ -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
|
||||
|
@ -11,6 +11,9 @@ all
|
||||
# Because we use HTML to hide them on the website.
|
||||
exclude_rule 'MD002'
|
||||
|
||||
# MD007 Allow unordered list indentation
|
||||
exclude_rule 'MD007'
|
||||
|
||||
# MD013 Line length
|
||||
exclude_rule 'MD013'
|
||||
|
||||
@ -20,6 +23,9 @@ exclude_rule 'MD014'
|
||||
# MD028 Blank line inside blockquote
|
||||
exclude_rule 'MD028'
|
||||
|
||||
# MD012 Multiple consecutive blank lines
|
||||
exclude_rule 'MD012'
|
||||
|
||||
# Tell the linter to use ordered lists:
|
||||
# 1. Foo
|
||||
# 2. Bar
|
||||
|
@ -12,16 +12,18 @@ You are looking at the HTTPie packaging documentation, where you will find valua
|
||||
|
||||
The overall release process starts simple:
|
||||
|
||||
1. Do the [PyPI](https://pypi.org/project/httpie/) publication.
|
||||
2. Then, handle company-related tasks.
|
||||
3. Finally, follow OS-specific steps, described in documents below, to send patches downstream.
|
||||
1. Bump the version identifiers in the following places:
|
||||
- `httpie/__init__.py`
|
||||
- `docs/packaging/windows-chocolatey/httpie.nuspec`
|
||||
- `CHANGELOG.md`
|
||||
2. Commit your changes and make a PR against the `master`.
|
||||
3. Merge the PR, and tag the last commit with your version identifier.
|
||||
4. Make a GitHub release (by copying the text in `CHANGELOG.md`)
|
||||
5. Push that release to PyPI (dispatch the `Release PyPI` GitHub action).
|
||||
6. Once PyPI is ready, push the release to the Snap, Homebrew and Chocolatey with their respective actions.
|
||||
7. Go to the [`httpie/debian.httpie.io`](https://github.com/httpie/debian.httpie.io) repo and trigger the package index workflow.
|
||||
|
||||
## First, PyPI
|
||||
|
||||
Let's do the release on [PyPi](https://pypi.org/project/httpie/).
|
||||
That is done quite easily by manually triggering the [release workflow](https://github.com/httpie/httpie/actions/workflows/release.yml).
|
||||
|
||||
## Then, company-specific tasks
|
||||
## Company-specific tasks
|
||||
|
||||
- Blank the `master_and_released_docs_differ_after` value in [config.json](https://github.com/httpie/httpie/blob/master/docs/config.json).
|
||||
- Update the [contributors list](../contributors).
|
||||
@ -36,10 +38,9 @@ A more complete state of deployment can be found on [repology](https://repology.
|
||||
| -------------------------------------------: | -------------- |
|
||||
| [Arch Linux, and derived](linux-arch/) | trusted person |
|
||||
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
|
||||
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
|
||||
| [Fedora](linux-fedora/) | trusted person |
|
||||
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
||||
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
|
||||
| [Debian, Ubuntu, and derived](linux-debian/) | **HTTPie** |
|
||||
| [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
||||
| [Snapcraft](snapcraft/) | **HTTPie** |
|
||||
| [Windows — Chocolatey](windows-chocolatey/) | **HTTPie** |
|
||||
|
||||
|
@ -13,21 +13,19 @@ We will discuss setting up the environment, installing development tools, instal
|
||||
|
||||
## Overall process
|
||||
|
||||
:construction: Work in progress.
|
||||
The brew deployment is completely automated, and only requires a trigger to [`Release on Homebrew`](https://github.com/httpie/httpie/actions/workflows/release-brew.yml) action
|
||||
from the release manager.
|
||||
|
||||
First, update the current Formula:
|
||||
If it is needed to be done manually, the following command can be used:
|
||||
|
||||
```bash
|
||||
make brew-deps
|
||||
# Copy-paste content into downstream/mac/brew/httpie.rb
|
||||
git add downstream/mac/brew/httpie.rb
|
||||
git commit -s -m 'Update brew formula to XXX'
|
||||
```console
|
||||
$ brew bump-formula-pr httpie --version={TARGET_VERSION}
|
||||
```
|
||||
|
||||
That [GitHub workflow](https://github.com/httpie/httpie/actions/workflows/test-package-mac-brew.yml) will test the formula when `downstream/mac/brew/httpie.rb` is changed in a pull request.
|
||||
|
||||
Then, open a pull request with those changes to the [downstream file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb).
|
||||
which will bump the formula, and create a PR against the package index.
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
||||
Make your changes, test the formula through the [`Test Brew Package`](https://github.com/httpie/httpie/actions/workflows/test-package-mac-brew.yml) action
|
||||
and then finally submit your patch to [`homebrew-core`](https://github.com/Homebrew/homebrew-core`)
|
||||
|
||||
|
@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate Ruby code with URLs and file hashes for packages from PyPi
|
||||
(i.e., httpie itself as well as its dependencies) to be included
|
||||
in the Homebrew formula after a new release of HTTPie has been published
|
||||
on PyPi.
|
||||
|
||||
<https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb>
|
||||
|
||||
"""
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
VERSIONS = {
|
||||
# By default, we use the latest packages. But sometimes Requests has a maximum supported versions.
|
||||
# Take a look here before making a release: <https://github.com/psf/requests/blob/master/setup.py>
|
||||
'idna': '3.2',
|
||||
}
|
||||
|
||||
|
||||
# Note: Keep that list sorted.
|
||||
PACKAGES = [
|
||||
'certifi',
|
||||
'charset-normalizer',
|
||||
'defusedxml',
|
||||
'httpie',
|
||||
'idna',
|
||||
'Pygments',
|
||||
'PySocks',
|
||||
'requests',
|
||||
'requests-toolbelt',
|
||||
'urllib3',
|
||||
'multidict',
|
||||
]
|
||||
|
||||
|
||||
def get_package_meta(package_name):
|
||||
api_url = f'https://pypi.org/pypi/{package_name}/json'
|
||||
resp = requests.get(api_url).json()
|
||||
hasher = hashlib.sha256()
|
||||
version = VERSIONS.get(package_name)
|
||||
if package_name not in VERSIONS:
|
||||
# Latest version
|
||||
release_bundle = resp['urls']
|
||||
else:
|
||||
release_bundle = resp['releases'][version]
|
||||
|
||||
for release in release_bundle:
|
||||
download_url = release['url']
|
||||
if download_url.endswith('.tar.gz'):
|
||||
hasher.update(requests.get(download_url).content)
|
||||
return {
|
||||
'name': package_name,
|
||||
'url': download_url,
|
||||
'sha256': hasher.hexdigest(),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(f'{package_name}: download not found: {resp}')
|
||||
|
||||
|
||||
def main():
|
||||
package_meta_map = {
|
||||
package_name: get_package_meta(package_name)
|
||||
for package_name in PACKAGES
|
||||
}
|
||||
httpie_meta = package_meta_map.pop('httpie')
|
||||
print()
|
||||
print(' url "{url}"'.format(url=httpie_meta['url']))
|
||||
print(' sha256 "{sha256}"'.format(sha256=httpie_meta['sha256']))
|
||||
print()
|
||||
for dep_meta in package_meta_map.values():
|
||||
print(' resource "{name}" do'.format(name=dep_meta['name']))
|
||||
print(' url "{url}"'.format(url=dep_meta['url']))
|
||||
print(' sha256 "{sha256}"'.format(sha256=dep_meta['sha256']))
|
||||
print(' end')
|
||||
print('')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -3,19 +3,18 @@ class Httpie < Formula
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.io/"
|
||||
url "https://files.pythonhosted.org/packages/53/96/cbcfec73c186f076e4443faf3d91cbbc868f18f6323703afd348b1aba46d/httpie-2.6.0.tar.gz"
|
||||
sha256 "ef929317b239bbf0a5bb7159b4c5d2edbfc55f8a0bcf9cd24ce597daec2afca5"
|
||||
url "https://files.pythonhosted.org/packages/32/85/bb095699be20cc98731261cb80884e9458178f8fef2a38273530ce77c0a5/httpie-3.1.0.tar.gz"
|
||||
sha256 "2e4a2040b84a912e65c01fb34f7aafe88cad2a3af2da8c685ca65080f376feda"
|
||||
license "BSD-3-Clause"
|
||||
head "https://github.com/httpie/httpie.git", branch: "master"
|
||||
|
||||
bottle do
|
||||
sha256 cellar: :any_skip_relocation, arm64_monterey: "83aab05ffbcd4c3baa6de6158d57ebdaa67c148bef8c872527d90bdaebff0504"
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "3c3a5c2458d0658e14b663495e115297c573aa3466d292f12d02c3ec13a24bdf"
|
||||
sha256 cellar: :any_skip_relocation, monterey: "f860e7d3b77dca4928a2c5e10c4cbd50d792330dfb99f7d736ca0da9fb9dd0d0"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "377b0643aa1f6d310ba4cfc70d66a94cc458213db8d134940d3b10a32defacf1"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "6d306c30f6f1d7a551d88415efe12b7c3f25d0602f3579dc632771a463f78fa5"
|
||||
sha256 cellar: :any_skip_relocation, mojave: "f66b8cdff9cb7b44a84197c3e3d81d810f7ff8f2188998b977ccadfc7e2ec893"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "53f036b0114814c28982e8c022dcf494e7024de088641d7076fd73d12a45a0e9"
|
||||
sha256 cellar: :any_skip_relocation, arm64_monterey: "9bb6e8c1ef5ba8b019ddedd7e908dd2174da695351aa9a238dfb28b0f57ef005"
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "47ffccd3241155d863e1b4f6259d538a34d42a0cdeed8152bda257ee607b51be"
|
||||
sha256 cellar: :any_skip_relocation, monterey: "dc4a04cb05a9cd1bfa6a632a0e4a21975905954af54ece41f9050c52474267be"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "ae469e37864e967e0fd99fba15a78e719dcb351b462f98f3843c78ed1473df6d"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "291a3eaecb2a2cc845c1652686a9a14b21053d7e3a7d0115245b2150ca2e199e"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "710836e27c44c8e3ad181d668f4a9f78c4cb4c355d7b148a397599a7cd42713d"
|
||||
end
|
||||
|
||||
depends_on "python@3.10"
|
||||
@ -26,8 +25,8 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
resource "charset-normalizer" do
|
||||
url "https://files.pythonhosted.org/packages/48/44/76b179e0d1afe6e6a91fd5661c284f60238987f3b42b676d141d01cd5b97/charset-normalizer-2.0.10.tar.gz"
|
||||
sha256 "876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"
|
||||
url "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz"
|
||||
sha256 "2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"
|
||||
end
|
||||
|
||||
resource "defusedxml" do
|
||||
@ -36,8 +35,13 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz"
|
||||
sha256 "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
url "https://files.pythonhosted.org/packages/62/08/e3fc7c8161090f742f504f40b1bccbfc544d4a4e09eb774bf40aafce5436/idna-3.3.tar.gz"
|
||||
sha256 "9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
end
|
||||
|
||||
resource "multidict" do
|
||||
url "https://files.pythonhosted.org/packages/fa/a7/71c253cdb8a1528802bac7503bf82fe674367e4055b09c28846fdfa4ab90/multidict-6.0.2.tar.gz"
|
||||
sha256 "5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"
|
||||
end
|
||||
|
||||
resource "Pygments" do
|
||||
@ -65,20 +69,14 @@ class Httpie < Formula
|
||||
sha256 "0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||
end
|
||||
|
||||
resource "multidict" do
|
||||
url "https://files.pythonhosted.org/packages/8e/7c/e12a69795b7b7d5071614af2c691c97fbf16a2a513c66ec52dd7d0a115bb/multidict-5.2.0.tar.gz"
|
||||
sha256 "0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"
|
||||
end
|
||||
|
||||
def install
|
||||
virtualenv_install_with_resources
|
||||
end
|
||||
|
||||
test do
|
||||
# shell_output() already checks the status code
|
||||
shell_output("#{bin}/httpie -v")
|
||||
shell_output("#{bin}/https -v")
|
||||
shell_output("#{bin}/http -v")
|
||||
assert_match version.to_s, shell_output("#{bin}/httpie --version")
|
||||
assert_match version.to_s, shell_output("#{bin}/https --version")
|
||||
assert_match version.to_s, shell_output("#{bin}/http --version")
|
||||
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
|
6
docs/packaging/brew/update.sh
Executable file
6
docs/packaging/brew/update.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
rm -f httpie.rb
|
||||
http --download https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb
|
@ -11,19 +11,16 @@ Welcome to the documentation about **packaging HTTPie for Debian GNU/Linux**.
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Debian GNU/Linux. They apply to Ubuntu as well, and any Debian-derived distributions like MX Linux, Linux Mint, deepin, Pop!_OS, KDE neon, Zorin OS, elementary OS, Kubuntu, Devuan, Linux Lite, Peppermint OS, Lubuntu, antiX, Xubuntu, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is Bartosz Fenski.
|
||||
We create the standalone binaries (see this [for more details](../../../extras/packaging/linux/)) and package them with
|
||||
[FPM](https://github.com/jordansissel/fpm)'s `dir` mode. The core `http`/`https` commands don't have any dependencies, but the `httpie`
|
||||
command (due to the underlying `httpie cli plugins` interface) explicitly depends to the system Python (through `python3`/`python3-pip`).
|
||||
|
||||
## Overall process
|
||||
|
||||
Open a new bug on the Debian Bug Tracking System by sending an email:
|
||||
The [`Release as Standalone Linux Binary`](https://github.com/httpie/httpie/actions/workflows/release-linux-standalone.yml) will be automatically
|
||||
triggered when a new release is created, and it will submit the `.deb` package as a release asset.
|
||||
|
||||
- To: `Debian Bug Tracking System <submit@bugs.debian.org>`
|
||||
- Subject: `httpie: Version XXX available`
|
||||
- Message template (examples [1](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=993937), and [2](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=996479)):
|
||||
|
||||
```email
|
||||
Package: httpie
|
||||
Severity: normal
|
||||
|
||||
<MESSAGE>
|
||||
```
|
||||
For making that asset available for all debian users, the release manager needs to go to the [`httpie/debian.httpie.io`](https://github.com/httpie/debian.httpie.io) repo
|
||||
and trigger the [`Update Index`](https://github.com/httpie/debian.httpie.io/actions/workflows/update-index.yml) action. It will automatically
|
||||
scrape all new debian packages from the release assets, properly update the indexes and create a new PR ([an example](https://github.com/httpie/debian.httpie.io/pull/1))
|
||||
which then will become active when merged.
|
||||
|
@ -1,5 +1,5 @@
|
||||
Name: httpie
|
||||
Version: 2.6.0
|
||||
Version: 3.1.0
|
||||
Release: 1%{?dist}
|
||||
Summary: A Curl-like tool for humans
|
||||
|
||||
@ -78,6 +78,25 @@ help2man %{buildroot}%{_bindir}/httpie > %{buildroot}%{_mandir}/man1/httpie.1
|
||||
|
||||
|
||||
%changelog
|
||||
* Tue Mar 08 2022 Miro Hrončok <mhroncok@redhat.com> - 3.1.0-1
|
||||
- Update to 3.1.0
|
||||
- Fixes: rhbz#2061597
|
||||
|
||||
* Mon Jan 24 2022 Miro Hrončok <mhroncok@redhat.com> - 3.0.2-1
|
||||
- Update to 3.0.2
|
||||
- Fixes: rhbz#2044572
|
||||
|
||||
* Mon Jan 24 2022 Miro Hrončok <mhroncok@redhat.com> - 3.0.1-1
|
||||
- Update to 3.0.1
|
||||
- Fixes: rhbz#2044058
|
||||
|
||||
* Fri Jan 21 2022 Miro Hrončok <mhroncok@redhat.com> - 3.0.0-1
|
||||
- Update to 3.0.0
|
||||
- Fixes: rhbz#2043680
|
||||
|
||||
* Thu Jan 20 2022 Fedora Release Engineering <releng@fedoraproject.org> - 2.6.0-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild
|
||||
|
||||
* Fri Oct 15 2021 Miro Hrončok <mhroncok@redhat.com> - 2.6.0-1
|
||||
- Update to 2.6.0
|
||||
- Fixes: rhbz#2014022
|
||||
|
6
docs/packaging/linux-fedora/update.sh
Executable file
6
docs/packaging/linux-fedora/update.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
rm -f httpie.spec.txt
|
||||
https --download src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -o httpie.spec.txt
|
@ -13,7 +13,16 @@ We will discuss setting up the environment, installing development tools, instal
|
||||
|
||||
## Overall process
|
||||
|
||||
Trigger a new [build](https://snapcraft.io/httpie/builds), then [promote it](https://snapcraft.io/httpie/releases). If more management is needed: [revisions supervision](https://dashboard.snapcraft.io/snaps/httpie/revisions/).
|
||||
Trigger the [`Release on Snap`](https://github.com/httpie/httpie/actions/workflows/release-snap.yml) action, which will
|
||||
create a snap package for HTTPie and then push it to Snap Store in the following channels:
|
||||
|
||||
- Edge
|
||||
- Beta
|
||||
- Candidate
|
||||
- Stable
|
||||
|
||||
If a push to any of them fail, all the release tasks for the following channels will be cancelled so that the
|
||||
release manager can look into the underlying cause.
|
||||
|
||||
## Hacking
|
||||
|
||||
|
@ -13,13 +13,18 @@ We will discuss setting up the environment, installing development tools, instal
|
||||
|
||||
## Overall process
|
||||
|
||||
After having successfully [built and tested](#hacking) the package, push it:
|
||||
After having successfully [built and tested](#hacking) the package, either trigger the
|
||||
[`Release on Chocolatey`](https://github.com/httpie/httpie/actions/workflows/release-choco.yml) action
|
||||
to push it to the `Chocolatey` store or use the CLI:
|
||||
|
||||
```bash
|
||||
# Replace 2.5.0 with the correct version
|
||||
choco push httpie.2.5.0.nupkg -s https://push.chocolatey.org/ --api-key=API_KEY
|
||||
```
|
||||
|
||||
Be aware that it might take multiple days until the release is approved, sine it goes through multiple
|
||||
sets of reviews (some of them are done manually).
|
||||
|
||||
## Hacking
|
||||
|
||||
```bash
|
||||
|
@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>httpie</id>
|
||||
<version>2.6.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.
|
||||
@ -29,11 +29,11 @@ Main features:
|
||||
<title>HTTPie</title>
|
||||
<authors>HTTPie</authors>
|
||||
<owners>jakubroztocil</owners>
|
||||
<copyright>2012-2021 Jakub Roztocil</copyright>
|
||||
<copyright>2012-2022 Jakub Roztocil</copyright>
|
||||
<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/blob/2.6.0/CHANGELOG.md).</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>
|
||||
|
BIN
docs/stardust.png
Normal file
BIN
docs/stardust.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
@ -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'
|
||||
@ -70,26 +43,28 @@ complete -c http -s x -l compress -d 'Content compressed with Deflate algorithm'
|
||||
|
||||
# Output Processing
|
||||
|
||||
complete -c http -l pretty -xa "all colors format none" -d 'Controls output processing'
|
||||
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 format-options -x -d 'Controls output formatting'
|
||||
complete -c http -l pretty -xa "all colors format none" -d 'Controls output processing'
|
||||
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 h -l headers -d 'Print only the response headers'
|
||||
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'
|
||||
complete -c http -s c -l continue -d 'Resume an interrupted download'
|
||||
complete -c http -s q -l quiet -d 'Do not print to stdout or stderr'
|
||||
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 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'
|
||||
complete -c http -s c -l continue -d 'Resume an interrupted download'
|
||||
complete -c http -s q -l quiet -d 'Do not print to stdout or stderr'
|
||||
|
||||
|
||||
# Sessions
|
||||
@ -115,22 +90,24 @@ 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
|
||||
|
||||
complete -c http -l verify -xa "(__fish_http_verify_options)" -d 'Enable/disable cert verification'
|
||||
complete -c http -l ssl -x -d 'Desired protocol version to use'
|
||||
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 verify -xa "(__fish_http_verify_options)" -d 'Enable/disable cert verification'
|
||||
complete -c http -l ssl -x -d 'Desired protocol version to use'
|
||||
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'
|
||||
|
598
extras/man/http.1
Normal file
598
extras/man/http.1
Normal file
@ -0,0 +1,598 @@
|
||||
.\" 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
|
||||
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
|
||||
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
|
||||
.IP "\fB\,METHOD\/\fR"
|
||||
|
||||
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
This argument can be omitted in which case HTTPie will use POST if there
|
||||
is some data to be sent, otherwise GET:
|
||||
|
||||
$ http example.org # => GET
|
||||
$ http example.org hello=world # => POST
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,URL\/\fR"
|
||||
|
||||
|
||||
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
|
||||
|
||||
$ http :3000 # => http://localhost:3000
|
||||
$ http :/foo # => http://localhost/foo
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,REQUEST_ITEM\/\fR"
|
||||
|
||||
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
\[aq]:\[aq] HTTP headers:
|
||||
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
\[aq]==\[aq] URL parameters to be appended to the request URI:
|
||||
|
||||
search==httpie
|
||||
|
||||
\[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=\[aq]CLI HTTP client\[aq]
|
||||
|
||||
\[aq]:=\[aq] Non-string JSON data fields (only with \fB\,--json\/\fR, \fB\,-j\/\fR):
|
||||
|
||||
awesome:=true amount:=42 colors:=\[aq][\[dq]red\[dq], \[dq]green\[dq], \[dq]blue\[dq]]\[aq]
|
||||
|
||||
\[aq]@\[aq] Form file fields (only with \fB\,--form\/\fR or \fB\,--multipart\/\fR):
|
||||
|
||||
cv@\(ti/Documents/CV.pdf
|
||||
cv@\[aq]\(ti/Documents/CV.pdf;type=application/pdf\[aq]
|
||||
|
||||
\[aq]=@\[aq] A data field like \[aq]=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
\[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\e:colon=value
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Predefined content types
|
||||
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||
|
||||
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
(if not specified).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--form\/\fR, \fB\,-f\/\fR"
|
||||
|
||||
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||
specified). The presence of any file fields results in a
|
||||
multipart/form-data request.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--multipart\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
.IP "\fB\,--raw\/\fR"
|
||||
|
||||
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
|
||||
$ http \fB\,--raw\/\fR=\[aq]data\[aq] pie.dev/post
|
||||
|
||||
You can achieve the same by piping the data via stdin:
|
||||
|
||||
$ echo data | http pie.dev/post
|
||||
|
||||
Or have HTTPie load the raw data from a file:
|
||||
|
||||
$ http pie.dev/post @data.txt
|
||||
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Content processing options
|
||||
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||
|
||||
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
|
||||
Compression is skipped if it appears that compression ratio is
|
||||
negative. Compression can be forced by repeating the argument.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output processing
|
||||
.IP "\fB\,--pretty\/\fR"
|
||||
|
||||
|
||||
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 \[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
|
||||
|
||||
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 \[dq]xterm-256color\[dq] or similar
|
||||
(e.g., via `export TERM=xterm-256color\[aq] in your \(ti/.bashrc).
|
||||
|
||||
.IP "\fB\,--unsorted\/\fR"
|
||||
|
||||
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
\fB\,--format-options\/\fR=headers.sort:false,json.sort_keys:false
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--sorted\/\fR"
|
||||
|
||||
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
\fB\,--format-options\/\fR=headers.sort:true,json.sort_keys:true
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--response-charset\/\fR \fI\,ENCODING\/\fR"
|
||||
|
||||
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
\fB\,--response-charset\/\fR=utf8
|
||||
\fB\,--response-charset\/\fR=big5
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--response-mime\/\fR \fI\,MIME_TYPE\/\fR"
|
||||
|
||||
|
||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||
|
||||
\fB\,--response-mime\/\fR=application/json
|
||||
\fB\,--response-mime\/\fR=text/xml
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--format-options\/\fR"
|
||||
|
||||
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) \fB\,--pretty\/\fR=all or \fB\,--pretty\/\fR=format.
|
||||
The following are the default options:
|
||||
|
||||
headers.sort:true
|
||||
json.format:true
|
||||
json.indent:4
|
||||
json.sort_keys:true
|
||||
xml.format:true
|
||||
xml.indent:2
|
||||
|
||||
You may use this option multiple times, as well as specify multiple
|
||||
comma-separated options at the same time. For example, this modifies the
|
||||
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||
|
||||
\fB\,--format-options\/\fR json.sort_keys:false,json.indent:2
|
||||
|
||||
This is something you will typically put into your config file.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output options
|
||||
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||
|
||||
|
||||
String specifying what the output should contain:
|
||||
|
||||
\[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 \[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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
.IP "\fB\,--all\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--stream\/\fR, \fB\,-S\/\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),
|
||||
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
|
||||
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
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--download\/\fR, \fB\,-d\/\fR"
|
||||
|
||||
|
||||
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
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||
|
||||
|
||||
Resume an interrupted download. Note that the \fB\,--output\/\fR option needs to be
|
||||
specified as well.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--quiet\/\fR, \fB\,-q\/\fR"
|
||||
|
||||
|
||||
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\[aq]t affect behaviour of download beyond not printing to terminal.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Sessions
|
||||
.IP "\fB\,--session\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||
|
||||
|
||||
Create, or reuse and update a session. Within a session, custom headers,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
Session files are stored in:
|
||||
|
||||
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||
|
||||
https://httpie.io/docs/cli/config-file-directory
|
||||
|
||||
|
||||
.IP "\fB\,--session-read-only\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||
|
||||
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Authentication
|
||||
.IP "\fB\,--auth\/\fR, \fB\,-a\/\fR \fI\,USER[:PASS] | TOKEN\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||
|
||||
|
||||
The authentication mechanism to be used. Defaults to \[dq]basic\[dq].
|
||||
|
||||
\[dq]basic\[dq]: Basic HTTP auth
|
||||
|
||||
\[dq]digest\[dq]: Digest HTTP auth
|
||||
|
||||
\[dq]bearer\[dq]: Bearer HTTP Auth
|
||||
|
||||
For finding out all available authentication types in your system, try:
|
||||
|
||||
$ http \fB\,--auth-type\/\fR
|
||||
|
||||
.IP "\fB\,--ignore-netrc\/\fR"
|
||||
|
||||
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Network
|
||||
.IP "\fB\,--offline\/\fR"
|
||||
|
||||
|
||||
Build the request and print it but don\(gat actually send it.
|
||||
|
||||
|
||||
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||
|
||||
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||
and $HTTPS_proxy are supported as well.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--follow\/\fR, \fB\,-F\/\fR"
|
||||
|
||||
|
||||
Follow 30x Location redirects.
|
||||
|
||||
|
||||
.IP "\fB\,--max-redirects\/\fR"
|
||||
|
||||
|
||||
By default, requests have a limit of 30 redirects (works with \fB\,--follow\/\fR).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--max-headers\/\fR"
|
||||
|
||||
|
||||
The maximum number of response headers to be read before giving up (default 0, i.e., no limit).
|
||||
|
||||
|
||||
.IP "\fB\,--timeout\/\fR \fI\,SECONDS\/\fR"
|
||||
|
||||
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
This is not a time limit on the entire response download;
|
||||
rather, an error is reported if the server has not issued a response for
|
||||
timeout seconds (more precisely, if no bytes have been received on
|
||||
the underlying socket for timeout seconds).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--check-status\/\fR"
|
||||
|
||||
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
|
||||
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\[aq]t been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--path-as-is\/\fR"
|
||||
|
||||
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
|
||||
.IP "\fB\,--chunked\/\fR"
|
||||
|
||||
|
||||
Enable streaming via chunked transfer encoding. The Transfer-Encoding header is set to chunked.
|
||||
|
||||
|
||||
.PP
|
||||
.SH SSL
|
||||
.IP "\fB\,--verify\/\fR"
|
||||
|
||||
|
||||
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.)
|
||||
|
||||
|
||||
.IP "\fB\,--ssl\/\fR"
|
||||
|
||||
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. Available protocols
|
||||
may vary depending on OpenSSL installation (only the supported ones
|
||||
are shown here).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--ciphers\/\fR"
|
||||
|
||||
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
is used:
|
||||
|
||||
ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key-pass\/\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\(gall be prompted interactively.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Troubleshooting
|
||||
.IP "\fB\,--ignore-stdin\/\fR, \fB\,-I\/\fR"
|
||||
|
||||
|
||||
Do not attempt to read stdin
|
||||
|
||||
|
||||
.IP "\fB\,--help\/\fR"
|
||||
|
||||
|
||||
Show this help message and exit.
|
||||
|
||||
|
||||
.IP "\fB\,--manual\/\fR"
|
||||
|
||||
|
||||
Show the full manual.
|
||||
|
||||
|
||||
.IP "\fB\,--version\/\fR"
|
||||
|
||||
|
||||
Show version and exit.
|
||||
|
||||
|
||||
.IP "\fB\,--traceback\/\fR"
|
||||
|
||||
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
|
||||
.IP "\fB\,--default-scheme\/\fR"
|
||||
|
||||
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
|
||||
.IP "\fB\,--debug\/\fR"
|
||||
|
||||
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
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
|
100
extras/man/httpie.1
Normal file
100
extras/man/httpie.1
Normal file
@ -0,0 +1,100 @@
|
||||
.\" 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
|
||||
.SH DESCRIPTION
|
||||
|
||||
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
|
||||
|
||||
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.
|
||||
.IP "\fB\,HOSTNAME\/\fR"
|
||||
|
||||
The host this session belongs.
|
||||
|
||||
.IP "\fB\,SESSION_NAME_OR_PATH\/\fR"
|
||||
|
||||
The name or the path for the session that will be upgraded.
|
||||
|
||||
.IP "\fB\,--bind-cookies\/\fR"
|
||||
|
||||
Bind domainless cookies to the host that session belongs.
|
||||
|
||||
.PP
|
||||
.SH httpie cli sessions upgrade-all
|
||||
Upgrade all named sessions with the latest layout. A list of changes between different session versions can be found in the official documentation.
|
||||
.IP "\fB\,--bind-cookies\/\fR"
|
||||
|
||||
Bind domainless cookies to the host that session belongs.
|
||||
|
||||
.PP
|
||||
.SH httpie cli plugins install
|
||||
Install the given targets from PyPI or from a local paths.
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to install
|
||||
|
||||
.PP
|
||||
.SH httpie cli plugins upgrade
|
||||
Upgrade the given plugins
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to upgrade
|
||||
|
||||
.PP
|
||||
.SH httpie cli plugins uninstall
|
||||
Uninstall the given HTTPie plugins.
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to install
|
||||
|
||||
.PP
|
||||
.SH httpie cli plugins list
|
||||
List all installed HTTPie plugins.
|
||||
.PP
|
||||
.SH httpie plugins install
|
||||
Install the given targets from PyPI or from a local paths.
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to install
|
||||
|
||||
.PP
|
||||
.SH httpie plugins upgrade
|
||||
Upgrade the given plugins
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to upgrade
|
||||
|
||||
.PP
|
||||
.SH httpie plugins uninstall
|
||||
Uninstall the given HTTPie plugins.
|
||||
.IP "\fB\,TARGET\/\fR"
|
||||
|
||||
targets to install
|
||||
|
||||
.PP
|
||||
.SH httpie plugins list
|
||||
List all installed HTTPie plugins.
|
||||
.PP
|
598
extras/man/https.1
Normal file
598
extras/man/https.1
Normal file
@ -0,0 +1,598 @@
|
||||
.\" 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
|
||||
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
|
||||
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
|
||||
.IP "\fB\,METHOD\/\fR"
|
||||
|
||||
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
This argument can be omitted in which case HTTPie will use POST if there
|
||||
is some data to be sent, otherwise GET:
|
||||
|
||||
$ http example.org # => GET
|
||||
$ http example.org hello=world # => POST
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,URL\/\fR"
|
||||
|
||||
|
||||
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
|
||||
|
||||
$ http :3000 # => http://localhost:3000
|
||||
$ http :/foo # => http://localhost/foo
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,REQUEST_ITEM\/\fR"
|
||||
|
||||
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
\[aq]:\[aq] HTTP headers:
|
||||
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
\[aq]==\[aq] URL parameters to be appended to the request URI:
|
||||
|
||||
search==httpie
|
||||
|
||||
\[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=\[aq]CLI HTTP client\[aq]
|
||||
|
||||
\[aq]:=\[aq] Non-string JSON data fields (only with \fB\,--json\/\fR, \fB\,-j\/\fR):
|
||||
|
||||
awesome:=true amount:=42 colors:=\[aq][\[dq]red\[dq], \[dq]green\[dq], \[dq]blue\[dq]]\[aq]
|
||||
|
||||
\[aq]@\[aq] Form file fields (only with \fB\,--form\/\fR or \fB\,--multipart\/\fR):
|
||||
|
||||
cv@\(ti/Documents/CV.pdf
|
||||
cv@\[aq]\(ti/Documents/CV.pdf;type=application/pdf\[aq]
|
||||
|
||||
\[aq]=@\[aq] A data field like \[aq]=\[aq], but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
\[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\e:colon=value
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Predefined content types
|
||||
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||
|
||||
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
(if not specified).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--form\/\fR, \fB\,-f\/\fR"
|
||||
|
||||
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||
specified). The presence of any file fields results in a
|
||||
multipart/form-data request.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--multipart\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
.IP "\fB\,--raw\/\fR"
|
||||
|
||||
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
|
||||
$ http \fB\,--raw\/\fR=\[aq]data\[aq] pie.dev/post
|
||||
|
||||
You can achieve the same by piping the data via stdin:
|
||||
|
||||
$ echo data | http pie.dev/post
|
||||
|
||||
Or have HTTPie load the raw data from a file:
|
||||
|
||||
$ http pie.dev/post @data.txt
|
||||
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Content processing options
|
||||
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||
|
||||
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
|
||||
Compression is skipped if it appears that compression ratio is
|
||||
negative. Compression can be forced by repeating the argument.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output processing
|
||||
.IP "\fB\,--pretty\/\fR"
|
||||
|
||||
|
||||
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 \[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
|
||||
|
||||
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 \[dq]xterm-256color\[dq] or similar
|
||||
(e.g., via `export TERM=xterm-256color\[aq] in your \(ti/.bashrc).
|
||||
|
||||
.IP "\fB\,--unsorted\/\fR"
|
||||
|
||||
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
\fB\,--format-options\/\fR=headers.sort:false,json.sort_keys:false
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--sorted\/\fR"
|
||||
|
||||
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
\fB\,--format-options\/\fR=headers.sort:true,json.sort_keys:true
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--response-charset\/\fR \fI\,ENCODING\/\fR"
|
||||
|
||||
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
\fB\,--response-charset\/\fR=utf8
|
||||
\fB\,--response-charset\/\fR=big5
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--response-mime\/\fR \fI\,MIME_TYPE\/\fR"
|
||||
|
||||
|
||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||
|
||||
\fB\,--response-mime\/\fR=application/json
|
||||
\fB\,--response-mime\/\fR=text/xml
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--format-options\/\fR"
|
||||
|
||||
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) \fB\,--pretty\/\fR=all or \fB\,--pretty\/\fR=format.
|
||||
The following are the default options:
|
||||
|
||||
headers.sort:true
|
||||
json.format:true
|
||||
json.indent:4
|
||||
json.sort_keys:true
|
||||
xml.format:true
|
||||
xml.indent:2
|
||||
|
||||
You may use this option multiple times, as well as specify multiple
|
||||
comma-separated options at the same time. For example, this modifies the
|
||||
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||
|
||||
\fB\,--format-options\/\fR json.sort_keys:false,json.indent:2
|
||||
|
||||
This is something you will typically put into your config file.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Output options
|
||||
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||
|
||||
|
||||
String specifying what the output should contain:
|
||||
|
||||
\[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 \[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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
.IP "\fB\,--all\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--stream\/\fR, \fB\,-S\/\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),
|
||||
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
|
||||
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
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--download\/\fR, \fB\,-d\/\fR"
|
||||
|
||||
|
||||
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
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||
|
||||
|
||||
Resume an interrupted download. Note that the \fB\,--output\/\fR option needs to be
|
||||
specified as well.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--quiet\/\fR, \fB\,-q\/\fR"
|
||||
|
||||
|
||||
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\[aq]t affect behaviour of download beyond not printing to terminal.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Sessions
|
||||
.IP "\fB\,--session\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||
|
||||
|
||||
Create, or reuse and update a session. Within a session, custom headers,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
Session files are stored in:
|
||||
|
||||
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||
|
||||
https://httpie.io/docs/cli/config-file-directory
|
||||
|
||||
|
||||
.IP "\fB\,--session-read-only\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||
|
||||
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
|
||||
|
||||
|
||||
.PP
|
||||
.SH Authentication
|
||||
.IP "\fB\,--auth\/\fR, \fB\,-a\/\fR \fI\,USER[:PASS] | TOKEN\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||
|
||||
|
||||
The authentication mechanism to be used. Defaults to \[dq]basic\[dq].
|
||||
|
||||
\[dq]basic\[dq]: Basic HTTP auth
|
||||
|
||||
\[dq]digest\[dq]: Digest HTTP auth
|
||||
|
||||
\[dq]bearer\[dq]: Bearer HTTP Auth
|
||||
|
||||
For finding out all available authentication types in your system, try:
|
||||
|
||||
$ http \fB\,--auth-type\/\fR
|
||||
|
||||
.IP "\fB\,--ignore-netrc\/\fR"
|
||||
|
||||
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Network
|
||||
.IP "\fB\,--offline\/\fR"
|
||||
|
||||
|
||||
Build the request and print it but don\(gat actually send it.
|
||||
|
||||
|
||||
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||
|
||||
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||
and $HTTPS_proxy are supported as well.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--follow\/\fR, \fB\,-F\/\fR"
|
||||
|
||||
|
||||
Follow 30x Location redirects.
|
||||
|
||||
|
||||
.IP "\fB\,--max-redirects\/\fR"
|
||||
|
||||
|
||||
By default, requests have a limit of 30 redirects (works with \fB\,--follow\/\fR).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--max-headers\/\fR"
|
||||
|
||||
|
||||
The maximum number of response headers to be read before giving up (default 0, i.e., no limit).
|
||||
|
||||
|
||||
.IP "\fB\,--timeout\/\fR \fI\,SECONDS\/\fR"
|
||||
|
||||
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
This is not a time limit on the entire response download;
|
||||
rather, an error is reported if the server has not issued a response for
|
||||
timeout seconds (more precisely, if no bytes have been received on
|
||||
the underlying socket for timeout seconds).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--check-status\/\fR"
|
||||
|
||||
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
|
||||
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\[aq]t been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--path-as-is\/\fR"
|
||||
|
||||
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
|
||||
.IP "\fB\,--chunked\/\fR"
|
||||
|
||||
|
||||
Enable streaming via chunked transfer encoding. The Transfer-Encoding header is set to chunked.
|
||||
|
||||
|
||||
.PP
|
||||
.SH SSL
|
||||
.IP "\fB\,--verify\/\fR"
|
||||
|
||||
|
||||
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.)
|
||||
|
||||
|
||||
.IP "\fB\,--ssl\/\fR"
|
||||
|
||||
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. Available protocols
|
||||
may vary depending on OpenSSL installation (only the supported ones
|
||||
are shown here).
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--ciphers\/\fR"
|
||||
|
||||
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
is used:
|
||||
|
||||
ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key\/\fR"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
.IP "\fB\,--cert-key-pass\/\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\(gall be prompted interactively.
|
||||
|
||||
|
||||
.PP
|
||||
.SH Troubleshooting
|
||||
.IP "\fB\,--ignore-stdin\/\fR, \fB\,-I\/\fR"
|
||||
|
||||
|
||||
Do not attempt to read stdin
|
||||
|
||||
|
||||
.IP "\fB\,--help\/\fR"
|
||||
|
||||
|
||||
Show this help message and exit.
|
||||
|
||||
|
||||
.IP "\fB\,--manual\/\fR"
|
||||
|
||||
|
||||
Show the full manual.
|
||||
|
||||
|
||||
.IP "\fB\,--version\/\fR"
|
||||
|
||||
|
||||
Show version and exit.
|
||||
|
||||
|
||||
.IP "\fB\,--traceback\/\fR"
|
||||
|
||||
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
|
||||
.IP "\fB\,--default-scheme\/\fR"
|
||||
|
||||
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
|
||||
.IP "\fB\,--debug\/\fR"
|
||||
|
||||
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
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
|
33
extras/packaging/linux/Dockerfile
Normal file
33
extras/packaging/linux/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# Use the oldest (but still supported) Ubuntu as the base for PyInstaller
|
||||
# packages. This will prevent stuff like glibc from conflicting.
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y software-properties-common binutils
|
||||
RUN apt-get install -y ruby-dev
|
||||
RUN gem install fpm
|
||||
|
||||
# Use deadsnakes for the latest Pythons (e.g 3.9)
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
RUN apt-get update && apt-get install -y python3.9 python3.9-dev python3.9-venv
|
||||
|
||||
# Install rpm as well, since we are going to build fedora dists too
|
||||
RUN apt-get install -y rpm
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app/extras/packaging/linux
|
||||
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
RUN python3.9 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
# Ensure that pip is renewed, otherwise we would be using distro-provided pip
|
||||
# which strips vendored packages and doesn't work with PyInstaller.
|
||||
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"]
|
52
extras/packaging/linux/README.md
Normal file
52
extras/packaging/linux/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Standalone Linux Packages
|
||||
|
||||

|
||||
|
||||
This directory contains the build scripts for creating:
|
||||
|
||||
- A self-contained binary executable for the HTTPie itself
|
||||
- `httpie.deb` and `httpie.rpm` packages for Debian and Fedora.
|
||||
|
||||
The process of constructing them are fully automated, and can be easily done through the [`Release as Standalone Linux Package`](https://github.com/httpie/httpie/actions/workflows/release-linux-standalone.yml)
|
||||
action. Once it finishes, the release artifacts will be attached in the summary page of the triggered run.
|
||||
|
||||
|
||||
## Hacking
|
||||
|
||||
The main entry point for the package builder is the [`build.py`](https://github.com/httpie/httpie/blob/master/extras/packaging/linux/build.py). It
|
||||
contains 2 major methods:
|
||||
|
||||
- `build_binaries`, for the self-contained executables
|
||||
- `build_packages`, for the OS-specific packages (which wrap the binaries)
|
||||
|
||||
### `build_binaries`
|
||||
|
||||
We use [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/) for the binaries. Normally pyinstaller offers two different modes:
|
||||
|
||||
- Single directory (harder to distribute, low redundancy. Library files are shared across different executables)
|
||||
- Single binary (easier to distribute, higher redundancy. Same libraries are statically linked to different executables, so higher total size)
|
||||
|
||||
Since our binary size (in total 20 MiBs) is not that big, we have decided to choose the single binary mode for the sake of easier distribution.
|
||||
|
||||
We also disable `UPX`, which is a runtime decompression method since it adds some startup cost.
|
||||
|
||||
### `build_packages`
|
||||
|
||||
We build our OS-specific packages with [FPM](https://github.com/jordansissel/fpm) which offers a really nice abstraction. We use the `dir` mode,
|
||||
and package `http`, `https` and `httpie` commands. More can be added to the `files` option.
|
||||
|
||||
Since the `httpie` depends on having a pip executable, we explicitly depend on the system Python even though the core does not use it.
|
||||
|
||||
### Docker Image
|
||||
|
||||
This directory also contains a [docker image](https://github.com/httpie/httpie/blob/master/extras/packaging/linux/Dockerfile) which helps
|
||||
building our standalone binaries in an isolated environment with the lowest possible library versions. This is important, since even though
|
||||
the executables are standalone they still depend on some main system C libraries (like `glibc`) so we need to create our executables inside
|
||||
an environment with a very old (but not deprecated) glibc version. It makes us soundproof for all active Ubuntu/Debian versions.
|
||||
|
||||
It also contains the Python version we package our HTTPie with, so it is the place if you need to change it.
|
||||
|
||||
### `./get_release_artifacts.sh`
|
||||
|
||||
If you make a change in the `build.py`, run the following script to test it out. It will return multiple files under `artifacts/dist` which
|
||||
then you can test out and ensure their quality (it is also the script that we use in our automation).
|
109
extras/packaging/linux/build.py
Normal file
109
extras/packaging/linux/build.py
Normal file
@ -0,0 +1,109 @@
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
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'
|
||||
|
||||
DIST_DIR = BUILD_DIR / 'dist'
|
||||
|
||||
TARGET_SCRIPTS = {
|
||||
SCRIPT_DIR / 'http_cli.py': [],
|
||||
SCRIPT_DIR / 'httpie_cli.py': ['--hidden-import=pip'],
|
||||
}
|
||||
|
||||
|
||||
def build_binaries() -> Iterator[Tuple[str, Path]]:
|
||||
for target_script, extra_args in TARGET_SCRIPTS.items():
|
||||
subprocess.check_call(
|
||||
[
|
||||
'pyinstaller',
|
||||
'--onefile',
|
||||
'--noupx',
|
||||
'-p',
|
||||
HTTPIE_DIR,
|
||||
'--additional-hooks-dir',
|
||||
HOOKS_DIR,
|
||||
*extra_args,
|
||||
target_script,
|
||||
]
|
||||
)
|
||||
|
||||
for executable_path in DIST_DIR.iterdir():
|
||||
if executable_path.suffix:
|
||||
continue
|
||||
stat_r = executable_path.stat()
|
||||
executable_path.chmod(stat_r.st_mode | stat.S_IEXEC)
|
||||
yield executable_path.stem, executable_path
|
||||
|
||||
|
||||
def build_packages(http_binary: Path, httpie_binary: Path) -> None:
|
||||
import httpie
|
||||
|
||||
# Mapping of src_file -> dst_file
|
||||
files = [
|
||||
(http_binary, '/usr/bin/http'),
|
||||
(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',
|
||||
'python3-pip'
|
||||
]
|
||||
|
||||
processed_deps = [
|
||||
f'--depends={dep}'
|
||||
for dep in deps
|
||||
]
|
||||
processed_files = [
|
||||
'='.join([str(src.resolve()), dst]) for src, dst in files
|
||||
]
|
||||
for target in ['deb', 'rpm']:
|
||||
subprocess.check_call(
|
||||
[
|
||||
'fpm',
|
||||
'--force',
|
||||
'-s',
|
||||
'dir',
|
||||
'-t',
|
||||
target,
|
||||
'--name',
|
||||
'httpie',
|
||||
'--version',
|
||||
httpie.__version__,
|
||||
'--description',
|
||||
httpie.__doc__.strip(),
|
||||
'--license',
|
||||
httpie.__licence__,
|
||||
*processed_deps,
|
||||
*processed_files,
|
||||
],
|
||||
cwd=DIST_DIR,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
binaries = dict(build_binaries())
|
||||
build_packages(binaries['http_cli'], binaries['httpie_cli'])
|
||||
|
||||
# Rename http_cli/httpie_cli to http/httpie
|
||||
binaries['http_cli'].rename(DIST_DIR / 'http')
|
||||
binaries['httpie_cli'].rename(DIST_DIR / 'httpie')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
22
extras/packaging/linux/get_release_artifacts.sh
Executable file
22
extras/packaging/linux/get_release_artifacts.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
REPO_ROOT=../../../
|
||||
ARTIFACTS_DIR=$(pwd)/artifacts
|
||||
|
||||
# Reset the ARTIFACTS_DIR.
|
||||
rm -rf $ARTIFACTS_DIR
|
||||
mkdir -p $ARTIFACTS_DIR
|
||||
|
||||
# Operate on the repository root to have the proper
|
||||
# docker context.
|
||||
pushd $REPO_ROOT
|
||||
|
||||
# Build the PyInstaller image
|
||||
docker build -t pyinstaller-httpie -f extras/packaging/linux/Dockerfile .
|
||||
|
||||
# Copy the artifacts to the designated directory.
|
||||
docker run --rm -i -v $ARTIFACTS_DIR:/artifacts pyinstaller-httpie:latest
|
||||
|
||||
popd
|
14
extras/packaging/linux/scripts/hooks/hook-pip.py
Normal file
14
extras/packaging/linux/scripts/hooks/hook-pip.py
Normal file
@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
def hook(hook_api):
|
||||
for pkg in [
|
||||
'pip',
|
||||
'setuptools',
|
||||
'distutils',
|
||||
'pkg_resources'
|
||||
]:
|
||||
datas, binaries, hiddenimports = collect_all(pkg)
|
||||
hook_api.add_datas(datas)
|
||||
hook_api.add_binaries(binaries)
|
||||
hook_api.add_imports(*hiddenimports)
|
5
extras/packaging/linux/scripts/http_cli.py
Normal file
5
extras/packaging/linux/scripts/http_cli.py
Normal file
@ -0,0 +1,5 @@
|
||||
from httpie.__main__ import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(main())
|
5
extras/packaging/linux/scripts/httpie_cli.py
Normal file
5
extras/packaging/linux/scripts/httpie_cli.py
Normal file
@ -0,0 +1,5 @@
|
||||
from httpie.manager.__main__ import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.exit(main())
|
39
extras/profiling/README.md
Normal file
39
extras/profiling/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# HTTPie Benchmarking Infrastructure
|
||||
|
||||
This directory includes the benchmarks we use for testing HTTPie's speed and the
|
||||
infrastructure to automate this testing across versions.
|
||||
|
||||
## Usage
|
||||
|
||||
Ensure the following requirements are satisfied:
|
||||
|
||||
- Python 3.7+
|
||||
- `pyperf`
|
||||
|
||||
Then, run the `extras/benchmarks/run.py`:
|
||||
|
||||
```console
|
||||
$ python extras/profiling/run.py
|
||||
```
|
||||
|
||||
Without any options, this command will initially create an isolated environment
|
||||
and install `httpie` from the latest commit. Then it will create a second
|
||||
environment with the `master` of the current repository and run the benchmarks
|
||||
on both of them. It will compare the results and print it as a markdown table:
|
||||
|
||||
| Benchmark | master | this_branch |
|
||||
| -------------------------------------- | :----: | :------------------: |
|
||||
| `http --version` (startup) | 201 ms | 174 ms: 1.16x faster |
|
||||
| `http --offline pie.dev/get` (startup) | 200 ms | 174 ms: 1.15x faster |
|
||||
| Geometric mean | (ref) | 1.10x faster |
|
||||
|
||||
If your `master` branch is not up-to-date, you can get a fresh clone by passing
|
||||
`--fresh` option. This way, the benchmark runner will clone the `httpie/httpie`
|
||||
repo from `GitHub` and use it as the baseline.
|
||||
|
||||
You can customize these branches by passing `--local-repo`/`--target-branch`,
|
||||
and customize the repos by passing `--local-repo`/`--target-repo` (can either
|
||||
take a URL or a path).
|
||||
|
||||
If you want to run a third environment with additional dependencies (such as
|
||||
`pyOpenSSL`), you can pass `--complex`.
|
@ -21,7 +21,7 @@ Examples:
|
||||
$ python extras/profiling/benchmarks.py --fast
|
||||
|
||||
# For verify everything works as expected, pass --debug-single-value.
|
||||
# It will only run everything once, so the resuls are not realiable. But
|
||||
# It will only run everything once, so the resuls are not reliable. But
|
||||
# very useful when iterating on a benchmark
|
||||
$ python extras/profiling/benchmarks.py --debug-single-value
|
||||
|
||||
|
183
extras/scripts/generate_man_pages.py
Normal file
183
extras/scripts/generate_man_pages.py
Normal file
@ -0,0 +1,183 @@
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Optional, Iterator, Iterable
|
||||
|
||||
import httpie
|
||||
from httpie.cli.definition import options as core_options
|
||||
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
|
||||
|
||||
|
||||
# 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]
|
||||
)
|
||||
|
||||
class ManPageBuilder:
|
||||
def __init__(self):
|
||||
self.source = []
|
||||
|
||||
def title_line(
|
||||
self,
|
||||
full_name: str,
|
||||
program_name: str,
|
||||
program_version: str,
|
||||
last_edit_date: str,
|
||||
) -> None:
|
||||
self.source.append(
|
||||
f'.TH {program_name} 1 "{last_edit_date}" '
|
||||
f'"{full_name} {program_version}" "{full_name} Manual"'
|
||||
)
|
||||
|
||||
def set_name(self, program_name: str) -> None:
|
||||
with self.section('NAME'):
|
||||
self.write(program_name)
|
||||
|
||||
def write(self, text: str, *, bold: bool = False) -> None:
|
||||
if bold:
|
||||
text = '.B ' + text
|
||||
self.source.append(text)
|
||||
|
||||
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:
|
||||
text += f' {self.underline(metavar)}'
|
||||
self.write(f'.IP "{text}"')
|
||||
|
||||
def build(self) -> str:
|
||||
return '\n'.join(self.source)
|
||||
|
||||
@contextmanager
|
||||
def section(self, section_name: str) -> Iterator[None]:
|
||||
self.write(f'.SH {section_name}')
|
||||
self.in_section = True
|
||||
yield
|
||||
self.in_section = False
|
||||
|
||||
def underline(self, text: str) -> str:
|
||||
return r'\fI\,{}\/\fR'.format(text)
|
||||
|
||||
def boldify(self, text: str) -> str:
|
||||
return r'\fB\,{}\/\fR'.format(text)
|
||||
|
||||
|
||||
def _escape_and_dedent(text: str) -> str:
|
||||
lines = []
|
||||
for should_act, line in enumerate(text.splitlines()):
|
||||
# Only dedent after the first line.
|
||||
if should_act:
|
||||
if line.startswith(' '):
|
||||
line = line[4:]
|
||||
|
||||
lines.append(line)
|
||||
return '\n'.join(lines).translate(ESCAPE_MAP)
|
||||
|
||||
|
||||
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',
|
||||
program_name=program_name,
|
||||
program_version=httpie.__version__,
|
||||
last_edit_date=httpie.__date__,
|
||||
)
|
||||
builder.set_name(program_name)
|
||||
|
||||
with builder.section('SYNOPSIS'):
|
||||
# `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):
|
||||
if group.description:
|
||||
builder.write(group.description)
|
||||
|
||||
for argument in group.arguments:
|
||||
if argument.is_hidden:
|
||||
continue
|
||||
|
||||
raw_arg = argument.serialize(isolation_mode=True)
|
||||
|
||||
metavar = raw_arg.get('metavar')
|
||||
if raw_arg.get('is_positional'):
|
||||
# In case of positional arguments, metavar is always equal
|
||||
# to the list of options (e.g `METHOD`).
|
||||
metavar = None
|
||||
builder.add_options(raw_arg['options'], metavar=metavar)
|
||||
|
||||
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, 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, **config))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -3,6 +3,7 @@ HTTPie: modern, user-friendly command-line HTTP client for the API era.
|
||||
|
||||
"""
|
||||
|
||||
__version__ = '3.0.0'
|
||||
__version__ = '3.2.1'
|
||||
__date__ = '2022-05-06'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -10,7 +10,8 @@ from urllib.parse import urlsplit
|
||||
from requests.utils import get_netrc_auth
|
||||
|
||||
from .argtypes import (
|
||||
AuthCredentials, KeyValueArgType, PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
AuthCredentials, SSLCredentials, KeyValueArgType,
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
parse_auth,
|
||||
parse_format_options,
|
||||
)
|
||||
@ -47,12 +48,39 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
|
||||
def add_usage(self, usage, actions, groups, prefix=None):
|
||||
# Only display the positional arguments
|
||||
displayed_actions = [
|
||||
action
|
||||
for action in actions
|
||||
if not action.option_strings
|
||||
]
|
||||
|
||||
_, exception, _ = sys.exc_info()
|
||||
if (
|
||||
isinstance(exception, argparse.ArgumentError)
|
||||
and len(exception.args) >= 1
|
||||
and isinstance(exception.args[0], argparse.Action)
|
||||
):
|
||||
# add_usage path is also taken when you pass an invalid option,
|
||||
# e.g --style=invalid. If something like that happens, we want
|
||||
# to include to action that caused to the invalid usage into
|
||||
# the list of actions we are displaying.
|
||||
displayed_actions.insert(0, exception.args[0])
|
||||
|
||||
super().add_usage(
|
||||
usage,
|
||||
displayed_actions,
|
||||
groups,
|
||||
prefix="usage:\n "
|
||||
)
|
||||
|
||||
|
||||
# TODO: refactor and design type-annotated data structures
|
||||
# for raw args + parsed args and keep things immutable.
|
||||
class BaseHTTPieArgumentParser(argparse.ArgumentParser):
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.env = None
|
||||
self.args = None
|
||||
self.has_stdin_data = False
|
||||
@ -115,9 +143,9 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
kwargs.setdefault('add_help', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(
|
||||
@ -127,6 +155,7 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.env.args = namespace = namespace or argparse.Namespace()
|
||||
self.args, no_options = super().parse_known_args(args, namespace)
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
@ -148,6 +177,7 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
self._parse_items()
|
||||
self._process_url()
|
||||
self._process_auth()
|
||||
self._process_ssl_cert()
|
||||
|
||||
if self.args.raw is not None:
|
||||
self._body_from_input(self.args.raw)
|
||||
@ -230,9 +260,24 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
if self.args.quiet:
|
||||
self.env.quiet = self.args.quiet
|
||||
self.env.stderr = self.env.devnull
|
||||
if not (self.args.output_file_specified and not self.args.download):
|
||||
self.env.stdout = self.env.devnull
|
||||
self.env.apply_warnings_filter()
|
||||
|
||||
def _process_ssl_cert(self):
|
||||
from httpie.ssl_ import _is_key_file_encrypted
|
||||
|
||||
if self.args.cert_key_pass is None:
|
||||
self.args.cert_key_pass = SSLCredentials(None)
|
||||
|
||||
if (
|
||||
self.args.cert_key is not None
|
||||
and self.args.cert_key_pass.value is None
|
||||
and _is_key_file_encrypted(self.args.cert_key)
|
||||
):
|
||||
self.args.cert_key_pass.prompt_password(self.args.cert_key)
|
||||
|
||||
def _process_auth(self):
|
||||
# TODO: refactor & simplify this method.
|
||||
@ -512,3 +557,57 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
for options_group in format_options:
|
||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||
self.args.format_options = parsed_options
|
||||
|
||||
def print_manual(self):
|
||||
from httpie.output.ui import man_pages
|
||||
|
||||
if man_pages.is_available(self.env.program_name):
|
||||
man_pages.display_for(self.env, self.env.program_name)
|
||||
return None
|
||||
|
||||
text = self.format_help()
|
||||
with self.env.rich_console.pager():
|
||||
self.env.rich_console.print(
|
||||
text,
|
||||
highlight=False
|
||||
)
|
||||
|
||||
def print_usage(self, file):
|
||||
from rich.text import Text
|
||||
from httpie.output.ui import rich_help
|
||||
|
||||
whitelist = set()
|
||||
_, exception, _ = sys.exc_info()
|
||||
if (
|
||||
isinstance(exception, argparse.ArgumentError)
|
||||
and len(exception.args) >= 1
|
||||
and isinstance(exception.args[0], argparse.Action)
|
||||
and exception.args[0].option_strings
|
||||
):
|
||||
# add_usage path is also taken when you pass an invalid option,
|
||||
# e.g --style=invalid. If something like that happens, we want
|
||||
# to include to action that caused to the invalid usage into
|
||||
# the list of actions we are displaying.
|
||||
whitelist.add(exception.args[0].option_strings[0])
|
||||
|
||||
usage_text = Text('usage', style='bold')
|
||||
usage_text.append(':\n ')
|
||||
usage_text.append(rich_help.to_usage(self.spec, whitelist=whitelist))
|
||||
self.env.rich_error_console.print(usage_text)
|
||||
|
||||
def error(self, message):
|
||||
"""Prints a usage message incorporating the message to stderr and
|
||||
exits."""
|
||||
self.print_usage(sys.stderr)
|
||||
self.env.rich_error_console.print(
|
||||
dedent(
|
||||
f'''
|
||||
[bold]error[/bold]:
|
||||
{message}
|
||||
|
||||
[bold]for more information[/bold]:
|
||||
run '{self.prog} --help' or visit https://httpie.io/docs/cli
|
||||
'''.rstrip()
|
||||
)
|
||||
)
|
||||
self.exit(2)
|
||||
|
@ -130,16 +130,11 @@ class KeyValueArgType:
|
||||
return tokens
|
||||
|
||||
|
||||
class AuthCredentials(KeyValueArg):
|
||||
"""Represents parsed credentials."""
|
||||
|
||||
def has_password(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
def prompt_password(self, host: str):
|
||||
prompt_text = f'http: password for {self.key}@{host}: '
|
||||
class PromptMixin:
|
||||
def _prompt_password(self, prompt: str) -> str:
|
||||
prompt_text = f'http: {prompt}: '
|
||||
try:
|
||||
self.value = self._getpass(prompt_text)
|
||||
return self._getpass(prompt_text)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(0)
|
||||
@ -150,6 +145,26 @@ class AuthCredentials(KeyValueArg):
|
||||
return getpass.getpass(str(prompt))
|
||||
|
||||
|
||||
class SSLCredentials(PromptMixin):
|
||||
"""Represents the passphrase for the certificate's key."""
|
||||
|
||||
def __init__(self, value: Optional[str]) -> None:
|
||||
self.value = value
|
||||
|
||||
def prompt_password(self, key_file: str) -> None:
|
||||
self.value = self._prompt_password(f'passphrase for {key_file}')
|
||||
|
||||
|
||||
class AuthCredentials(KeyValueArg, PromptMixin):
|
||||
"""Represents parsed credentials."""
|
||||
|
||||
def has_password(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
def prompt_password(self, host: str) -> None:
|
||||
self.value = self._prompt_password(f'password for {self.key}@{host}:')
|
||||
|
||||
|
||||
class AuthCredentialsArgType(KeyValueArgType):
|
||||
"""A key-value arg type that parses credentials."""
|
||||
|
||||
|
@ -9,6 +9,7 @@ URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
HTTP_OPTIONS = 'OPTIONS'
|
||||
|
||||
# Various separators used in args
|
||||
SEPARATOR_HEADER = ':'
|
||||
@ -90,13 +91,19 @@ OUTPUT_OPTIONS = frozenset({
|
||||
})
|
||||
|
||||
# Pretty
|
||||
|
||||
|
||||
class PrettyOptions(enum.Enum):
|
||||
STDOUT_TTY_ONLY = enum.auto()
|
||||
|
||||
|
||||
PRETTY_MAP = {
|
||||
'all': ['format', 'colors'],
|
||||
'colors': ['colors'],
|
||||
'format': ['format'],
|
||||
'none': []
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
PRETTY_STDOUT_TTY_ONLY = PrettyOptions.STDOUT_TTY_ONLY
|
||||
|
||||
|
||||
DEFAULT_FORMAT_OPTIONS = [
|
||||
@ -125,9 +132,3 @@ class RequestType(enum.Enum):
|
||||
FORM = enum.auto()
|
||||
MULTIPART = enum.auto()
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
OPEN_BRACKET = '['
|
||||
CLOSE_BRACKET = ']'
|
||||
BACKSLASH = '\\'
|
||||
HIGHLIGHTER = '^'
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,16 @@ class HTTPHeadersDict(CIMultiDict, BaseMultiDict):
|
||||
|
||||
super().add(key, value)
|
||||
|
||||
def remove_item(self, key, value):
|
||||
"""
|
||||
Remove a (key, value) pair from the dict.
|
||||
"""
|
||||
existing_values = self.popall(key)
|
||||
existing_values.remove(value)
|
||||
|
||||
for value in existing_values:
|
||||
self.add(key, value)
|
||||
|
||||
|
||||
class RequestJSONDataDict(OrderedDict):
|
||||
pass
|
||||
@ -82,3 +92,7 @@ class MultipartRequestDataDict(MultiValueOrderedDict):
|
||||
|
||||
class RequestFilesDict(RequestDataDict):
|
||||
pass
|
||||
|
||||
|
||||
class NestedJSONArray(list):
|
||||
"""Denotes a top-level JSON array."""
|
||||
|
@ -9,7 +9,14 @@ from typing import (
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from httpie.cli.constants import OPEN_BRACKET, CLOSE_BRACKET, BACKSLASH, HIGHLIGHTER
|
||||
from .dicts import NestedJSONArray
|
||||
|
||||
|
||||
EMPTY_STRING = ''
|
||||
HIGHLIGHTER = '^'
|
||||
OPEN_BRACKET = '['
|
||||
CLOSE_BRACKET = ']'
|
||||
BACKSLASH = '\\'
|
||||
|
||||
|
||||
class HTTPieSyntaxError(ValueError):
|
||||
@ -30,7 +37,7 @@ class HTTPieSyntaxError(ValueError):
|
||||
if self.token is not None:
|
||||
lines.append(self.source)
|
||||
lines.append(
|
||||
' ' * (self.token.start)
|
||||
' ' * self.token.start
|
||||
+ HIGHLIGHTER * (self.token.end - self.token.start)
|
||||
)
|
||||
return '\n'.join(lines)
|
||||
@ -50,8 +57,15 @@ class TokenKind(Enum):
|
||||
return 'a ' + self.name.lower()
|
||||
|
||||
|
||||
OPERATORS = {OPEN_BRACKET: TokenKind.LEFT_BRACKET, CLOSE_BRACKET: TokenKind.RIGHT_BRACKET}
|
||||
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):
|
||||
@ -88,18 +102,18 @@ def tokenize(source: str) -> Iterator[Token]:
|
||||
return None
|
||||
|
||||
value = ''.join(buffer)
|
||||
for variation, kind in [
|
||||
(int, TokenKind.NUMBER),
|
||||
(check_escaped_int, TokenKind.TEXT),
|
||||
]:
|
||||
try:
|
||||
value = variation(value)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
else:
|
||||
kind = TokenKind.TEXT
|
||||
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
|
||||
@ -171,8 +185,8 @@ class Path:
|
||||
|
||||
def parse(source: str) -> Iterator[Path]:
|
||||
"""
|
||||
start: literal? path*
|
||||
|
||||
start: root_path path*
|
||||
root_path: (literal | index_path | append_path)
|
||||
literal: TEXT | NUMBER
|
||||
|
||||
path:
|
||||
@ -215,16 +229,47 @@ def parse(source: str) -> Iterator[Path]:
|
||||
message = f'Expecting {suffix}'
|
||||
raise HTTPieSyntaxError(source, token, message)
|
||||
|
||||
root = Path(PathAction.KEY, '', is_root=True)
|
||||
if can_advance():
|
||||
token = tokens[cursor]
|
||||
if token.kind in {TokenKind.TEXT, TokenKind.NUMBER}:
|
||||
token = expect(TokenKind.TEXT, TokenKind.NUMBER)
|
||||
root.accessor = str(token.value)
|
||||
root.tokens.append(token)
|
||||
def parse_root():
|
||||
tokens = []
|
||||
if not can_advance():
|
||||
return Path(
|
||||
PathAction.KEY,
|
||||
EMPTY_STRING,
|
||||
is_root=True
|
||||
)
|
||||
|
||||
yield root
|
||||
# (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))
|
||||
@ -296,6 +341,10 @@ def interpret(context: Any, key: str, value: Any) -> Any:
|
||||
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:
|
||||
@ -337,8 +386,19 @@ def interpret(context: Any, key: str, value: Any) -> Any:
|
||||
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 = {}
|
||||
context = None
|
||||
for key, value in pairs:
|
||||
interpret(context, key, value)
|
||||
return context
|
||||
context = interpret(context, key, value)
|
||||
|
||||
return wrap_with_dict(context)
|
||||
|
249
httpie/cli/options.py
Normal file
249
httpie/cli/options.py
Normal file
@ -0,0 +1,249 @@
|
||||
import argparse
|
||||
import textwrap
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Optional, Dict, List, Tuple, Type, TypeVar
|
||||
|
||||
from httpie.cli.argparser import HTTPieArgumentParser
|
||||
from httpie.cli.utils import Manual, LazyChoices
|
||||
|
||||
|
||||
class Qualifiers(Enum):
|
||||
OPTIONAL = auto()
|
||||
ZERO_OR_MORE = auto()
|
||||
ONE_OR_MORE = auto()
|
||||
SUPPRESS = auto()
|
||||
|
||||
|
||||
def map_qualifiers(
|
||||
configuration: Dict[str, Any], qualifier_map: Dict[Qualifiers, Any]
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
key: qualifier_map[value] if isinstance(value, Qualifiers) else value
|
||||
for key, value in configuration.items()
|
||||
}
|
||||
|
||||
|
||||
def drop_keys(
|
||||
configuration: Dict[str, Any], key_blacklist: Tuple[str, ...]
|
||||
):
|
||||
return {
|
||||
key: value
|
||||
for key, value in configuration.items()
|
||||
if key not in key_blacklist
|
||||
}
|
||||
|
||||
|
||||
PARSER_SPEC_VERSION = '0.0.1a0'
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParserSpec:
|
||||
program: str
|
||||
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:
|
||||
self.description = textwrap.dedent(self.description)
|
||||
if self.epilog:
|
||||
self.epilog = textwrap.dedent(self.epilog)
|
||||
for group in self.groups:
|
||||
group.finalize()
|
||||
return self
|
||||
|
||||
def add_group(self, name: str, **kwargs) -> 'Group':
|
||||
group = Group(name, **kwargs)
|
||||
self.groups.append(group)
|
||||
return group
|
||||
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.program,
|
||||
'description': self.description,
|
||||
'groups': [group.serialize() for group in self.groups],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Group:
|
||||
name: str
|
||||
description: str = ''
|
||||
is_mutually_exclusive: bool = False
|
||||
arguments: List['Argument'] = field(default_factory=list)
|
||||
|
||||
def finalize(self) -> None:
|
||||
if self.description:
|
||||
self.description = textwrap.dedent(self.description)
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
argument = Argument(list(args), kwargs.copy())
|
||||
argument.post_init()
|
||||
self.arguments.append(argument)
|
||||
return argument
|
||||
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
'description': self.description or None,
|
||||
'is_mutually_exclusive': self.is_mutually_exclusive,
|
||||
'args': [argument.serialize() for argument in self.arguments],
|
||||
}
|
||||
|
||||
|
||||
class Argument(typing.NamedTuple):
|
||||
aliases: List[str]
|
||||
configuration: Dict[str, Any]
|
||||
|
||||
def post_init(self):
|
||||
"""Run a bunch of post-init hooks."""
|
||||
# If there is a short help, then create the longer version from it.
|
||||
short_help = self.configuration.get('short_help')
|
||||
if (
|
||||
short_help
|
||||
and 'help' not in self.configuration
|
||||
and self.configuration.get('action') != 'lazy_choices'
|
||||
):
|
||||
self.configuration['help'] = f'\n{short_help}\n\n'
|
||||
|
||||
def serialize(self, *, isolation_mode: bool = False) -> Dict[str, Any]:
|
||||
configuration = self.configuration.copy()
|
||||
|
||||
# Unpack the dynamically computed choices, since we
|
||||
# will need to store the actual values somewhere.
|
||||
action = configuration.pop('action', None)
|
||||
short_help = configuration.pop('short_help', None)
|
||||
nested_options = configuration.pop('nested_options', None)
|
||||
|
||||
if action == 'lazy_choices':
|
||||
choices = LazyChoices(
|
||||
self.aliases,
|
||||
**{'dest': None, **configuration},
|
||||
isolation_mode=isolation_mode
|
||||
)
|
||||
configuration['choices'] = list(choices.load())
|
||||
configuration['help'] = choices.help
|
||||
|
||||
result = {}
|
||||
if self.aliases:
|
||||
result['options'] = self.aliases.copy()
|
||||
else:
|
||||
result['options'] = [configuration['metavar']]
|
||||
result['is_positional'] = True
|
||||
|
||||
qualifiers = JSON_QUALIFIER_TO_OPTIONS[configuration.get('nargs', Qualifiers.SUPPRESS)]
|
||||
result.update(qualifiers)
|
||||
|
||||
description = configuration.get('help')
|
||||
if description and description is not Qualifiers.SUPPRESS:
|
||||
result['short_description'] = short_help
|
||||
result['description'] = description
|
||||
|
||||
if nested_options:
|
||||
result['nested_options'] = nested_options
|
||||
|
||||
python_type = configuration.get('type')
|
||||
if python_type is not None:
|
||||
if hasattr(python_type, '__name__'):
|
||||
type_name = python_type.__name__
|
||||
else:
|
||||
type_name = type(python_type).__name__
|
||||
|
||||
result['python_type_name'] = type_name
|
||||
|
||||
result.update({
|
||||
key: value
|
||||
for key, value in configuration.items()
|
||||
if key in JSON_DIRECT_MIRROR_OPTIONS
|
||||
if value is not Qualifiers.SUPPRESS
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def is_positional(self):
|
||||
return len(self.aliases) == 0
|
||||
|
||||
@property
|
||||
def is_hidden(self):
|
||||
return self.configuration.get('help') is Qualifiers.SUPPRESS
|
||||
|
||||
def __getattr__(self, attribute_name):
|
||||
if attribute_name in self.configuration:
|
||||
return self.configuration[attribute_name]
|
||||
else:
|
||||
raise AttributeError(attribute_name)
|
||||
|
||||
|
||||
ParserType = TypeVar('ParserType', bound=Type[argparse.ArgumentParser])
|
||||
|
||||
ARGPARSE_QUALIFIER_MAP = {
|
||||
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
||||
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
||||
Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE,
|
||||
Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE
|
||||
}
|
||||
ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options')
|
||||
|
||||
|
||||
def to_argparse(
|
||||
abstract_options: ParserSpec,
|
||||
parser_type: ParserType = HTTPieArgumentParser,
|
||||
) -> ParserType:
|
||||
concrete_parser = parser_type(
|
||||
prog=abstract_options.program,
|
||||
description=abstract_options.description,
|
||||
epilog=abstract_options.epilog,
|
||||
)
|
||||
concrete_parser.spec = abstract_options
|
||||
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
||||
concrete_parser.register('action', 'manual', Manual)
|
||||
|
||||
for abstract_group in abstract_options.groups:
|
||||
concrete_group = concrete_parser.add_argument_group(
|
||||
title=abstract_group.name, description=abstract_group.description
|
||||
)
|
||||
if abstract_group.is_mutually_exclusive:
|
||||
concrete_group = concrete_group.add_mutually_exclusive_group(required=False)
|
||||
|
||||
for abstract_argument in abstract_group.arguments:
|
||||
concrete_group.add_argument(
|
||||
*abstract_argument.aliases,
|
||||
**drop_keys(map_qualifiers(
|
||||
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
||||
), ARGPARSE_IGNORE_KEYS)
|
||||
)
|
||||
|
||||
return concrete_parser
|
||||
|
||||
|
||||
JSON_DIRECT_MIRROR_OPTIONS = (
|
||||
'choices',
|
||||
'metavar'
|
||||
)
|
||||
|
||||
|
||||
JSON_QUALIFIER_TO_OPTIONS = {
|
||||
Qualifiers.OPTIONAL: {'is_optional': True},
|
||||
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
||||
Qualifiers.ONE_OR_MORE: {'is_optional': False, 'is_variadic': True},
|
||||
Qualifiers.SUPPRESS: {}
|
||||
}
|
||||
|
||||
|
||||
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, **kwargs) -> ParserSpec:
|
||||
"""Take an existing argparse parser, and create a spec from it."""
|
||||
return ParserSpec(
|
||||
program=parser.prog,
|
||||
description=parser.description,
|
||||
epilog=parser.epilog,
|
||||
**kwargs
|
||||
)
|
@ -4,20 +4,43 @@ from typing import Any, Callable, Generic, Iterator, Iterable, Optional, TypeVar
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class Manual(argparse.Action):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings,
|
||||
dest=argparse.SUPPRESS,
|
||||
default=argparse.SUPPRESS,
|
||||
help=None
|
||||
):
|
||||
super().__init__(
|
||||
option_strings=option_strings,
|
||||
dest=dest,
|
||||
default=default,
|
||||
nargs=0,
|
||||
help=help
|
||||
)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
parser.print_manual()
|
||||
parser.exit()
|
||||
|
||||
|
||||
class LazyChoices(argparse.Action, Generic[T]):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
getter: Callable[[], Iterable[T]],
|
||||
help_formatter: Optional[Callable[[T], str]] = None,
|
||||
help_formatter: Optional[Callable[[T, bool], str]] = None,
|
||||
sort: bool = False,
|
||||
cache: bool = True,
|
||||
isolation_mode: bool = False,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.getter = getter
|
||||
self.help_formatter = help_formatter
|
||||
self.sort = sort
|
||||
self.cache = cache
|
||||
self.isolation_mode = isolation_mode
|
||||
self._help: Optional[str] = None
|
||||
self._obj: Optional[Iterable[T]] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -33,7 +56,10 @@ class LazyChoices(argparse.Action, Generic[T]):
|
||||
@property
|
||||
def help(self) -> str:
|
||||
if self._help is None and self.help_formatter is not None:
|
||||
self._help = self.help_formatter(self.load())
|
||||
self._help = self.help_formatter(
|
||||
self.load(),
|
||||
isolation_mode=self.isolation_mode
|
||||
)
|
||||
return self._help
|
||||
|
||||
@help.setter
|
||||
|
@ -3,6 +3,7 @@ import http.client
|
||||
import json
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from time import monotonic
|
||||
from typing import Any, Dict, Callable, Iterable
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
@ -12,12 +13,14 @@ import urllib3
|
||||
from . import __version__
|
||||
from .adapters import HTTPieHTTPAdapter
|
||||
from .context import Environment
|
||||
from .cli.dicts import HTTPHeadersDict
|
||||
from .cli.constants import HTTP_OPTIONS
|
||||
from .cli.nested_json import EMPTY_STRING
|
||||
from .cli.dicts import HTTPHeadersDict, NestedJSONArray
|
||||
from .encoding import UTF8
|
||||
from .models import RequestsMessage
|
||||
from .plugins.registry import plugin_manager
|
||||
from .sessions import get_httpie_session
|
||||
from .ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
|
||||
from .ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieCertificate, HTTPieHTTPSAdapter
|
||||
from .uploads import (
|
||||
compress_request, prepare_request_body,
|
||||
get_multipart_data_and_content_type,
|
||||
@ -32,6 +35,8 @@ JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ACCEPT = f'{JSON_CONTENT_TYPE}, */*;q=0.5'
|
||||
DEFAULT_UA = f'HTTPie/{__version__}'
|
||||
|
||||
IGNORE_CONTENT_LENGTH_METHODS = frozenset([HTTP_OPTIONS])
|
||||
|
||||
|
||||
def collect_messages(
|
||||
env: Environment,
|
||||
@ -42,6 +47,7 @@ def collect_messages(
|
||||
httpie_session_headers = None
|
||||
if args.session or args.session_read_only:
|
||||
httpie_session = get_httpie_session(
|
||||
env=env,
|
||||
config_dir=env.config.directory,
|
||||
session_name=args.session or args.session_read_only,
|
||||
host=args.headers.get('Host'),
|
||||
@ -82,7 +88,7 @@ def collect_messages(
|
||||
|
||||
request = requests.Request(**request_kwargs)
|
||||
prepared_request = requests_session.prepare_request(request)
|
||||
apply_missing_repeated_headers(prepared_request, request.headers)
|
||||
transform_headers(request, prepared_request)
|
||||
if args.path_as_is:
|
||||
prepared_request.url = ensure_path_as_is(
|
||||
orig_url=args.url,
|
||||
@ -108,7 +114,7 @@ def collect_messages(
|
||||
**send_kwargs_merged,
|
||||
**send_kwargs,
|
||||
)
|
||||
|
||||
response._httpie_headers_parsed_at = monotonic()
|
||||
expired_cookies += get_expired_cookies(
|
||||
response.headers.get('Set-Cookie', '')
|
||||
)
|
||||
@ -128,10 +134,7 @@ def collect_messages(
|
||||
if httpie_session:
|
||||
if httpie_session.is_new() or not args.session_read_only:
|
||||
httpie_session.cookies = requests_session.cookies
|
||||
httpie_session.remove_cookies(
|
||||
# TODO: take path & domain into account?
|
||||
cookie['name'] for cookie in expired_cookies
|
||||
)
|
||||
httpie_session.remove_cookies(expired_cookies)
|
||||
httpie_session.save()
|
||||
|
||||
|
||||
@ -200,9 +203,30 @@ def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
|
||||
return final_headers
|
||||
|
||||
|
||||
def transform_headers(
|
||||
request: requests.Request,
|
||||
prepared_request: requests.PreparedRequest
|
||||
) -> None:
|
||||
"""Apply various transformations on top of the `prepared_requests`'s
|
||||
headers to change the request prepreation behavior."""
|
||||
|
||||
# Remove 'Content-Length' when it is misplaced by requests.
|
||||
if (
|
||||
prepared_request.method in IGNORE_CONTENT_LENGTH_METHODS
|
||||
and prepared_request.headers.get('Content-Length') == '0'
|
||||
and request.headers.get('Content-Length') != '0'
|
||||
):
|
||||
prepared_request.headers.pop('Content-Length')
|
||||
|
||||
apply_missing_repeated_headers(
|
||||
request.headers,
|
||||
prepared_request
|
||||
)
|
||||
|
||||
|
||||
def apply_missing_repeated_headers(
|
||||
prepared_request: requests.PreparedRequest,
|
||||
original_headers: HTTPHeadersDict
|
||||
original_headers: HTTPHeadersDict,
|
||||
prepared_request: requests.PreparedRequest
|
||||
) -> None:
|
||||
"""Update the given `prepared_request`'s headers with the original
|
||||
ones. This allows the requests to be prepared as usual, and then later
|
||||
@ -260,7 +284,14 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
|
||||
if args.cert:
|
||||
cert = args.cert
|
||||
if args.cert_key:
|
||||
cert = cert, args.cert_key
|
||||
# Having a client certificate key passphrase is not supported
|
||||
# by requests. So we are using our own transportation structure
|
||||
# which is compatible with their format (a tuple of minimum two
|
||||
# items).
|
||||
#
|
||||
# See: https://github.com/psf/requests/issues/2519
|
||||
cert = HTTPieCertificate(cert, args.cert_key, args.cert_key_pass.value)
|
||||
|
||||
return {
|
||||
'proxies': {p.key: p.value for p in args.proxy},
|
||||
'stream': True,
|
||||
@ -279,7 +310,8 @@ def json_dict_to_request_body(data: Dict[str, Any]) -> str:
|
||||
# item in the object, with an en empty key.
|
||||
if len(data) == 1:
|
||||
[(key, value)] = data.items()
|
||||
if key == '' and isinstance(value, list):
|
||||
if isinstance(value, NestedJSONArray):
|
||||
assert key == EMPTY_STRING
|
||||
data = value
|
||||
|
||||
if data:
|
||||
|
@ -1,9 +1,21 @@
|
||||
import sys
|
||||
from typing import Any, Optional, Iterable
|
||||
|
||||
from httpie.cookies import HTTPieCookiePolicy
|
||||
from http import cookiejar # noqa
|
||||
|
||||
|
||||
# Request does not carry the original policy attached to the
|
||||
# cookie jar, so until it is resolved we change the global cookie
|
||||
# policy. <https://github.com/psf/requests/issues/5449>
|
||||
cookiejar.DefaultCookiePolicy = HTTPieCookiePolicy
|
||||
|
||||
|
||||
is_windows = 'win32' in str(sys.platform).lower()
|
||||
is_frozen = getattr(sys, 'frozen', False)
|
||||
|
||||
MIN_SUPPORTED_PY_VERSION = (3, 7)
|
||||
MAX_SUPPORTED_PY_VERSION = (3, 11)
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
|
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
from . import __version__
|
||||
from .compat import is_windows
|
||||
@ -62,6 +62,21 @@ class ConfigFileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def read_raw_config(config_type: str, path: Path) -> Dict[str, Any]:
|
||||
try:
|
||||
with path.open(encoding=UTF8) as f:
|
||||
try:
|
||||
return json.load(f)
|
||||
except ValueError as e:
|
||||
raise ConfigFileError(
|
||||
f'invalid {config_type} file: {e} [{path}]'
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise ConfigFileError(f'cannot read {config_type} file: {e}')
|
||||
|
||||
|
||||
class BaseConfigDict(dict):
|
||||
name = None
|
||||
helpurl = None
|
||||
@ -77,26 +92,25 @@ class BaseConfigDict(dict):
|
||||
def is_new(self) -> bool:
|
||||
return not self.path.exists()
|
||||
|
||||
def pre_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Hook for processing the incoming config data."""
|
||||
return data
|
||||
|
||||
def post_process_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Hook for processing the outgoing config data."""
|
||||
return data
|
||||
|
||||
def load(self):
|
||||
config_type = type(self).__name__.lower()
|
||||
try:
|
||||
with self.path.open(encoding=UTF8) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except ValueError as e:
|
||||
raise ConfigFileError(
|
||||
f'invalid {config_type} file: {e} [{self.path}]'
|
||||
)
|
||||
self.update(data)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise ConfigFileError(f'cannot read {config_type} file: {e}')
|
||||
data = read_raw_config(config_type, self.path)
|
||||
if data is not None:
|
||||
data = self.pre_process_data(data)
|
||||
self.update(data)
|
||||
|
||||
def save(self):
|
||||
self['__meta__'] = {
|
||||
'httpie': __version__
|
||||
}
|
||||
def save(self, *, bump_version: bool = False):
|
||||
self.setdefault('__meta__', {})
|
||||
if bump_version or 'httpie' not in self['__meta__']:
|
||||
self['__meta__']['httpie'] = __version__
|
||||
if self.helpurl:
|
||||
self['__meta__']['help'] = self.helpurl
|
||||
|
||||
@ -106,13 +120,19 @@ class BaseConfigDict(dict):
|
||||
self.ensure_directory()
|
||||
|
||||
json_string = json.dumps(
|
||||
obj=self,
|
||||
obj=self.post_process_data(self),
|
||||
indent=4,
|
||||
sort_keys=True,
|
||||
ensure_ascii=True,
|
||||
)
|
||||
self.path.write_text(json_string + '\n', encoding=UTF8)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self.get(
|
||||
'__meta__', {}
|
||||
).get('httpie', __version__)
|
||||
|
||||
|
||||
class Config(BaseConfigDict):
|
||||
FILENAME = 'config.json'
|
||||
@ -129,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')
|
||||
|
@ -1,8 +1,11 @@
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Iterator, IO, Optional
|
||||
from typing import Iterator, IO, Optional, TYPE_CHECKING
|
||||
from enum import Enum
|
||||
|
||||
|
||||
try:
|
||||
@ -10,11 +13,34 @@ try:
|
||||
except ImportError:
|
||||
curses = None # Compiled w/o curses
|
||||
|
||||
from .compat import is_windows
|
||||
from .compat import is_windows, cached_property
|
||||
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
||||
from .encoding import UTF8
|
||||
|
||||
from .utils import repr_dict
|
||||
from .output.ui.palette import GenericColor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console
|
||||
|
||||
|
||||
class LogLevel(str, Enum):
|
||||
INFO = 'info'
|
||||
WARNING = 'warning'
|
||||
ERROR = 'error'
|
||||
|
||||
|
||||
LOG_LEVEL_COLORS = {
|
||||
LogLevel.INFO: GenericColor.PINK,
|
||||
LogLevel.WARNING: GenericColor.ORANGE,
|
||||
LogLevel.ERROR: GenericColor.RED,
|
||||
}
|
||||
|
||||
LOG_LEVEL_DISPLAY_THRESHOLDS = {
|
||||
LogLevel.INFO: 1,
|
||||
LogLevel.WARNING: 2,
|
||||
LogLevel.ERROR: float('inf'), # Never hide errors.
|
||||
}
|
||||
|
||||
|
||||
class Environment:
|
||||
@ -27,6 +53,7 @@ class Environment:
|
||||
is used by the test suite to simulate various scenarios.
|
||||
|
||||
"""
|
||||
args = argparse.Namespace()
|
||||
is_windows: bool = is_windows
|
||||
config_dir: Path = DEFAULT_CONFIG_DIR
|
||||
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
||||
@ -39,6 +66,10 @@ class Environment:
|
||||
stderr_isatty: bool = stderr.isatty()
|
||||
colors = 256
|
||||
program_name: str = 'http'
|
||||
|
||||
# Whether to show progress bars / status spinners etc.
|
||||
show_displays: bool = True
|
||||
|
||||
if not is_windows:
|
||||
if curses:
|
||||
try:
|
||||
@ -87,6 +118,8 @@ class Environment:
|
||||
self.stdout_encoding = getattr(
|
||||
actual_stdout, 'encoding', None) or UTF8
|
||||
|
||||
self.quiet = kwargs.pop('quiet', 0)
|
||||
|
||||
def __str__(self):
|
||||
defaults = dict(type(self).__dict__)
|
||||
actual = dict(defaults)
|
||||
@ -134,6 +167,51 @@ class Environment:
|
||||
self.stdout = original_stdout
|
||||
self.stderr = original_stderr
|
||||
|
||||
def log_error(self, msg, level='error'):
|
||||
assert level in ['error', 'warning']
|
||||
self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')
|
||||
def log_error(self, msg: str, level: LogLevel = LogLevel.ERROR) -> None:
|
||||
if self.stdout_isatty and self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[level]:
|
||||
stderr = self.stderr # Not directly /dev/null, since stderr might be mocked
|
||||
else:
|
||||
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',
|
||||
style=LOG_LEVEL_COLORS[level],
|
||||
markup=False,
|
||||
highlight=False,
|
||||
soft_wrap=True
|
||||
)
|
||||
|
||||
def apply_warnings_filter(self) -> None:
|
||||
if self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[LogLevel.WARNING]:
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
def _make_rich_console(
|
||||
self,
|
||||
file: IO[str],
|
||||
force_terminal: bool
|
||||
) -> 'Console':
|
||||
from rich.console import Console
|
||||
from httpie.output.ui.rich_palette import _make_rich_color_theme
|
||||
|
||||
style = getattr(self.args, 'style', None)
|
||||
theme = _make_rich_color_theme(style)
|
||||
# Rich infers the rest of the knowledge (e.g encoding)
|
||||
# dynamically by looking at the file/stderr.
|
||||
return Console(
|
||||
file=file,
|
||||
force_terminal=force_terminal,
|
||||
no_color=(self.colors == 0),
|
||||
theme=theme
|
||||
)
|
||||
|
||||
# Rich recommends separating the actual console (stdout) from
|
||||
# the error (stderr) console for better isolation between parts.
|
||||
# https://rich.readthedocs.io/en/stable/console.html#error-console
|
||||
|
||||
@cached_property
|
||||
def rich_console(self):
|
||||
return self._make_rich_console(self.stdout, self.stdout_isatty)
|
||||
|
||||
@cached_property
|
||||
def rich_error_console(self):
|
||||
return self._make_rich_console(self.stderr, self.stderr_isatty)
|
||||
|
25
httpie/cookies.py
Normal file
25
httpie/cookies.py
Normal file
@ -0,0 +1,25 @@
|
||||
from http import cookiejar
|
||||
|
||||
|
||||
_LOCALHOST = 'localhost'
|
||||
_LOCALHOST_SUFFIX = '.localhost'
|
||||
|
||||
|
||||
class HTTPieCookiePolicy(cookiejar.DefaultCookiePolicy):
|
||||
def return_ok_secure(self, cookie, request):
|
||||
"""Check whether the given cookie is sent to a secure host."""
|
||||
|
||||
is_secure_protocol = super().return_ok_secure(cookie, request)
|
||||
if is_secure_protocol:
|
||||
return True
|
||||
|
||||
# The original implementation of this method only takes secure protocols
|
||||
# (e.g., https) into account, but the latest developments in modern browsers
|
||||
# (chrome, firefox) assume 'localhost' is also a secure location. So we
|
||||
# override it with our own strategy.
|
||||
return self._is_local_host(cookiejar.request_host(request))
|
||||
|
||||
def _is_local_host(self, hostname):
|
||||
# Implements the static localhost detection algorithm in firefox.
|
||||
# <https://searchfox.org/mozilla-central/rev/d4d7611ee4dd0003b492b865bc5988a4e6afc985/netwerk/dns/DNS.cpp#205-218>
|
||||
return hostname == _LOCALHOST or hostname.endswith(_LOCALHOST_SUFFIX)
|
@ -13,16 +13,19 @@ from . import __version__ as httpie_version
|
||||
from .cli.constants import OUT_REQ_BODY
|
||||
from .cli.nested_json import HTTPieSyntaxError
|
||||
from .client import collect_messages
|
||||
from .context import Environment
|
||||
from .context import Environment, LogLevel
|
||||
from .downloads import Downloader
|
||||
from .models import (
|
||||
RequestsMessageKind,
|
||||
OutputOptions,
|
||||
OutputOptions
|
||||
)
|
||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
||||
from .output.models import ProcessingOptions
|
||||
from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_SEPARATOR_BYTES
|
||||
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
|
||||
@ -30,14 +33,19 @@ def raw_main(
|
||||
parser: argparse.ArgumentParser,
|
||||
main_program: Callable[[argparse.Namespace, Environment], ExitStatus],
|
||||
args: List[Union[str, bytes]] = sys.argv,
|
||||
env: Environment = Environment()
|
||||
env: Environment = Environment(),
|
||||
use_default_options: bool = True,
|
||||
) -> ExitStatus:
|
||||
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 env.config.default_options:
|
||||
if use_default_options and env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
|
||||
include_debug_info = '--debug' in args
|
||||
@ -87,6 +95,7 @@ def raw_main(
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
else:
|
||||
check_updates(env)
|
||||
try:
|
||||
exit_status = main_program(
|
||||
args=parsed_args,
|
||||
@ -168,6 +177,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
downloader = None
|
||||
initial_request: Optional[requests.PreparedRequest] = None
|
||||
final_response: Optional[requests.Response] = None
|
||||
processing_options = ProcessingOptions.from_raw_args(args)
|
||||
|
||||
def separate():
|
||||
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
||||
@ -182,17 +192,17 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
and chunk
|
||||
)
|
||||
if should_pipe_to_stdout:
|
||||
msg = requests.PreparedRequest()
|
||||
msg.is_body_upload_chunk = True
|
||||
msg.body = chunk
|
||||
msg.headers = initial_request.headers
|
||||
msg_output_options = OutputOptions.from_message(msg, body=True, headers=False)
|
||||
write_message(requests_message=msg, env=env, args=args, output_options=msg_output_options)
|
||||
return write_raw_data(
|
||||
env,
|
||||
chunk,
|
||||
processing_options=processing_options,
|
||||
headers=initial_request.headers
|
||||
)
|
||||
|
||||
try:
|
||||
if args.download:
|
||||
args.follow = True # --download implies --follow.
|
||||
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
||||
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
|
||||
downloader.pre_request(args.headers)
|
||||
messages = collect_messages(env, args=args,
|
||||
request_body_read_callback=request_body_read_callback)
|
||||
@ -220,10 +230,15 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
if args.check_status or downloader:
|
||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
||||
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
|
||||
body=do_write_body
|
||||
))
|
||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING)
|
||||
write_message(
|
||||
requests_message=message,
|
||||
env=env,
|
||||
output_options=output_options._replace(
|
||||
body=do_write_body
|
||||
),
|
||||
processing_options=processing_options
|
||||
)
|
||||
prev_with_body = output_options.body
|
||||
|
||||
# Cleanup
|
||||
|
@ -5,10 +5,8 @@ Download mode implementation.
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from mailbox import Message
|
||||
from time import sleep, monotonic
|
||||
from time import monotonic
|
||||
from typing import IO, Optional, Tuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@ -16,22 +14,11 @@ import requests
|
||||
|
||||
from .models import HTTPResponse, OutputOptions
|
||||
from .output.streams import RawStream
|
||||
from .utils import humanize_bytes
|
||||
from .context import Environment
|
||||
|
||||
|
||||
PARTIAL_CONTENT = 206
|
||||
|
||||
CLEAR_LINE = '\r\033[K'
|
||||
PROGRESS = (
|
||||
'{percentage: 6.2f} %'
|
||||
' {downloaded: >10}'
|
||||
' {speed: >10}/s'
|
||||
' {eta: >8} ETA'
|
||||
)
|
||||
PROGRESS_NO_CONTENT_LENGTH = '{downloaded: >10} {speed: >10}/s'
|
||||
SUMMARY = 'Done. {downloaded} in {time:0.5f}s ({speed}/s)\n'
|
||||
SPINNER = '|/-\\'
|
||||
|
||||
|
||||
class ContentRangeError(ValueError):
|
||||
pass
|
||||
@ -176,9 +163,9 @@ class Downloader:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: Environment,
|
||||
output_file: IO = None,
|
||||
resume: bool = False,
|
||||
progress_file: IO = sys.stderr
|
||||
resume: bool = False
|
||||
):
|
||||
"""
|
||||
:param resume: Should the download resume if partial download
|
||||
@ -191,14 +178,10 @@ class Downloader:
|
||||
|
||||
"""
|
||||
self.finished = False
|
||||
self.status = DownloadStatus()
|
||||
self.status = DownloadStatus(env=env)
|
||||
self._output_file = output_file
|
||||
self._resume = resume
|
||||
self._resumed_from = 0
|
||||
self._progress_reporter = ProgressReporterThread(
|
||||
status=self.status,
|
||||
output=progress_file
|
||||
)
|
||||
|
||||
def pre_request(self, request_headers: dict):
|
||||
"""Called just before the HTTP request is sent.
|
||||
@ -261,11 +244,6 @@ class Downloader:
|
||||
except OSError:
|
||||
pass # stdout
|
||||
|
||||
self.status.started(
|
||||
resumed_from=self._resumed_from,
|
||||
total_size=total_size
|
||||
)
|
||||
|
||||
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
||||
stream = RawStream(
|
||||
msg=HTTPResponse(final_response),
|
||||
@ -273,11 +251,11 @@ class Downloader:
|
||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||
)
|
||||
|
||||
self._progress_reporter.output.write(
|
||||
f'Downloading {humanize_bytes(total_size) + " " if total_size is not None else ""}'
|
||||
f'to "{self._output_file.name}"\n'
|
||||
self.status.started(
|
||||
output_file=self._output_file,
|
||||
resumed_from=self._resumed_from,
|
||||
total_size=total_size
|
||||
)
|
||||
self._progress_reporter.start()
|
||||
|
||||
return stream, self._output_file
|
||||
|
||||
@ -287,7 +265,7 @@ class Downloader:
|
||||
self.status.finished()
|
||||
|
||||
def failed(self):
|
||||
self._progress_reporter.stop()
|
||||
self.status.terminate()
|
||||
|
||||
@property
|
||||
def interrupted(self) -> bool:
|
||||
@ -329,127 +307,71 @@ class Downloader:
|
||||
class DownloadStatus:
|
||||
"""Holds details about the download status."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
self.downloaded = 0
|
||||
self.total_size = None
|
||||
self.resumed_from = 0
|
||||
self.time_started = None
|
||||
self.time_finished = None
|
||||
|
||||
def started(self, resumed_from=0, total_size=None):
|
||||
def started(self, output_file, resumed_from=0, total_size=None):
|
||||
assert self.time_started is None
|
||||
self.total_size = total_size
|
||||
self.downloaded = self.resumed_from = resumed_from
|
||||
self.time_started = monotonic()
|
||||
self.start_display(output_file=output_file)
|
||||
|
||||
def start_display(self, output_file):
|
||||
from httpie.output.ui.rich_progress import (
|
||||
DummyDisplay,
|
||||
StatusDisplay,
|
||||
ProgressDisplay
|
||||
)
|
||||
|
||||
message = f'Downloading to {output_file.name}'
|
||||
if self.env.show_displays:
|
||||
if self.total_size is None:
|
||||
# Rich does not support progress bars without a total
|
||||
# size given. Instead we use status objects.
|
||||
self.display = StatusDisplay(self.env)
|
||||
else:
|
||||
self.display = ProgressDisplay(self.env)
|
||||
else:
|
||||
self.display = DummyDisplay(self.env)
|
||||
|
||||
self.display.start(
|
||||
total=self.total_size,
|
||||
at=self.downloaded,
|
||||
description=message
|
||||
)
|
||||
|
||||
def chunk_downloaded(self, size):
|
||||
assert self.time_finished is None
|
||||
self.downloaded += size
|
||||
self.display.update(size)
|
||||
|
||||
@property
|
||||
def has_finished(self):
|
||||
return self.time_finished is not None
|
||||
|
||||
@property
|
||||
def time_spent(self):
|
||||
if (
|
||||
self.time_started is not None
|
||||
and self.time_finished is not None
|
||||
):
|
||||
return self.time_finished - self.time_started
|
||||
else:
|
||||
return None
|
||||
|
||||
def finished(self):
|
||||
assert self.time_started is not None
|
||||
assert self.time_finished is None
|
||||
self.time_finished = monotonic()
|
||||
if hasattr(self, 'display'):
|
||||
self.display.stop(self.time_spent)
|
||||
|
||||
|
||||
class ProgressReporterThread(threading.Thread):
|
||||
"""
|
||||
Reports download progress based on its status.
|
||||
|
||||
Uses threading to periodically update the status (speed, ETA, etc.).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status: DownloadStatus,
|
||||
output: IO,
|
||||
tick=.1,
|
||||
update_interval=1
|
||||
):
|
||||
super().__init__()
|
||||
self.status = status
|
||||
self.output = output
|
||||
self._tick = tick
|
||||
self._update_interval = update_interval
|
||||
self._spinner_pos = 0
|
||||
self._status_line = ''
|
||||
self._prev_bytes = 0
|
||||
self._prev_time = monotonic()
|
||||
self._should_stop = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""Stop reporting on next tick."""
|
||||
self._should_stop.set()
|
||||
|
||||
def run(self):
|
||||
while not self._should_stop.is_set():
|
||||
if self.status.has_finished:
|
||||
self.sum_up()
|
||||
break
|
||||
|
||||
self.report_speed()
|
||||
sleep(self._tick)
|
||||
|
||||
def report_speed(self):
|
||||
now = monotonic()
|
||||
if now - self._prev_time >= self._update_interval:
|
||||
downloaded = self.status.downloaded
|
||||
speed = ((downloaded - self._prev_bytes)
|
||||
/ (now - self._prev_time))
|
||||
|
||||
if not self.status.total_size:
|
||||
self._status_line = PROGRESS_NO_CONTENT_LENGTH.format(
|
||||
downloaded=humanize_bytes(downloaded),
|
||||
speed=humanize_bytes(speed),
|
||||
)
|
||||
else:
|
||||
percentage = (downloaded / self.status.total_size * 100
|
||||
if self.status.total_size
|
||||
else 0)
|
||||
|
||||
if not speed:
|
||||
eta = '-:--:--'
|
||||
else:
|
||||
s = int((self.status.total_size - downloaded) / speed)
|
||||
h, s = divmod(s, 60 * 60)
|
||||
m, s = divmod(s, 60)
|
||||
eta = f'{h}:{m:0>2}:{s:0>2}'
|
||||
|
||||
self._status_line = PROGRESS.format(
|
||||
percentage=percentage,
|
||||
downloaded=humanize_bytes(downloaded),
|
||||
speed=humanize_bytes(speed),
|
||||
eta=eta,
|
||||
)
|
||||
|
||||
self._prev_time = now
|
||||
self._prev_bytes = downloaded
|
||||
|
||||
self.output.write(
|
||||
f'{CLEAR_LINE} {SPINNER[self._spinner_pos]} {self._status_line}'
|
||||
)
|
||||
self.output.flush()
|
||||
|
||||
self._spinner_pos = (self._spinner_pos + 1) % len(SPINNER)
|
||||
|
||||
def sum_up(self):
|
||||
actually_downloaded = (
|
||||
self.status.downloaded - self.status.resumed_from)
|
||||
time_taken = self.status.time_finished - self.status.time_started
|
||||
speed = actually_downloaded / time_taken if time_taken else actually_downloaded
|
||||
|
||||
self.output.write(CLEAR_LINE)
|
||||
|
||||
self.output.write(SUMMARY.format(
|
||||
downloaded=humanize_bytes(actually_downloaded),
|
||||
total=(self.status.total_size
|
||||
and humanize_bytes(self.status.total_size)),
|
||||
speed=humanize_bytes(speed),
|
||||
time=time_taken,
|
||||
))
|
||||
self.output.flush()
|
||||
def terminate(self):
|
||||
if hasattr(self, 'display'):
|
||||
self.display.stop(self.time_spent)
|
||||
|
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)
|
0
httpie/legacy/__init__.py
Normal file
0
httpie/legacy/__init__.py
Normal file
100
httpie/legacy/v3_1_0_session_cookie_format.py
Normal file
100
httpie/legacy/v3_1_0_session_cookie_format.py
Normal file
@ -0,0 +1,100 @@
|
||||
import argparse
|
||||
from typing import Any, Type, List, Dict, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from httpie.sessions import Session
|
||||
|
||||
|
||||
INSECURE_COOKIE_JAR_WARNING = '''\
|
||||
Outdated layout detected for the current session. Please consider updating it,
|
||||
in order to not get affected by potential security problems.
|
||||
|
||||
For fixing the current session:
|
||||
|
||||
With binding all cookies to the current host (secure):
|
||||
$ httpie cli sessions upgrade --bind-cookies {hostname} {session_id}
|
||||
|
||||
Without binding cookies (leaving them as is) (insecure):
|
||||
$ httpie cli sessions upgrade {hostname} {session_id}
|
||||
'''
|
||||
|
||||
|
||||
INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS = '''\
|
||||
|
||||
For fixing all named sessions:
|
||||
|
||||
With binding all cookies to the current host (secure):
|
||||
$ httpie cli sessions upgrade-all --bind-cookies
|
||||
|
||||
Without binding cookies (leaving them as is) (insecure):
|
||||
$ httpie cli sessions upgrade-all
|
||||
'''
|
||||
|
||||
INSECURE_COOKIE_SECURITY_LINK = '\nSee https://pie.co/docs/security for more information.'
|
||||
|
||||
|
||||
def pre_process(session: 'Session', cookies: Any) -> List[Dict[str, Any]]:
|
||||
"""Load the given cookies to the cookie jar while maintaining
|
||||
support for the old cookie layout."""
|
||||
|
||||
is_old_style = isinstance(cookies, dict)
|
||||
if is_old_style:
|
||||
normalized_cookies = [
|
||||
{
|
||||
'name': key,
|
||||
**value
|
||||
}
|
||||
for key, value in cookies.items()
|
||||
]
|
||||
else:
|
||||
normalized_cookies = cookies
|
||||
|
||||
should_issue_warning = is_old_style and any(
|
||||
cookie.get('domain', '') == ''
|
||||
for cookie in normalized_cookies
|
||||
)
|
||||
|
||||
if should_issue_warning:
|
||||
warning = INSECURE_COOKIE_JAR_WARNING.format(hostname=session.bound_host, session_id=session.session_id)
|
||||
if not session.is_anonymous:
|
||||
warning += INSECURE_COOKIE_JAR_WARNING_FOR_NAMED_SESSIONS
|
||||
warning += INSECURE_COOKIE_SECURITY_LINK
|
||||
session.warn_legacy_usage(warning)
|
||||
|
||||
return normalized_cookies
|
||||
|
||||
|
||||
def post_process(
|
||||
normalized_cookies: List[Dict[str, Any]],
|
||||
*,
|
||||
original_type: Type[Any]
|
||||
) -> Any:
|
||||
"""Convert the cookies to their original format for
|
||||
maximum compatibility."""
|
||||
|
||||
if issubclass(original_type, dict):
|
||||
return {
|
||||
cookie.pop('name'): cookie
|
||||
for cookie in normalized_cookies
|
||||
}
|
||||
else:
|
||||
return normalized_cookies
|
||||
|
||||
|
||||
def fix_layout(session: 'Session', hostname: str, args: argparse.Namespace) -> None:
|
||||
if not isinstance(session['cookies'], dict):
|
||||
return None
|
||||
|
||||
session['cookies'] = [
|
||||
{
|
||||
'name': key,
|
||||
**value
|
||||
}
|
||||
for key, value in session['cookies'].items()
|
||||
]
|
||||
for cookie in session.cookies:
|
||||
if cookie.domain == '':
|
||||
if args.bind_cookies:
|
||||
cookie.domain = hostname
|
||||
else:
|
||||
cookie._rest['is_explicit_none'] = True
|
73
httpie/legacy/v3_2_0_session_header_format.py
Normal file
73
httpie/legacy/v3_2_0_session_header_format.py
Normal file
@ -0,0 +1,73 @@
|
||||
from typing import Any, Type, List, Dict, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from httpie.sessions import Session
|
||||
|
||||
|
||||
OLD_HEADER_STORE_WARNING = '''\
|
||||
Outdated layout detected for the current session. Please consider updating it,
|
||||
in order to use the latest features regarding the header layout.
|
||||
|
||||
For fixing the current session:
|
||||
|
||||
$ httpie cli sessions upgrade {hostname} {session_id}
|
||||
'''
|
||||
|
||||
OLD_HEADER_STORE_WARNING_FOR_NAMED_SESSIONS = '''\
|
||||
|
||||
For fixing all named sessions:
|
||||
|
||||
$ httpie cli sessions upgrade-all
|
||||
'''
|
||||
|
||||
OLD_HEADER_STORE_LINK = '\nSee $INSERT_LINK for more information.'
|
||||
|
||||
|
||||
def pre_process(session: 'Session', headers: Any) -> List[Dict[str, Any]]:
|
||||
"""Serialize the headers into a unified form and issue a warning if
|
||||
the session file is using the old layout."""
|
||||
|
||||
is_old_style = isinstance(headers, dict)
|
||||
if is_old_style:
|
||||
normalized_headers = list(headers.items())
|
||||
else:
|
||||
normalized_headers = [
|
||||
(item['name'], item['value'])
|
||||
for item in headers
|
||||
]
|
||||
|
||||
if is_old_style:
|
||||
warning = OLD_HEADER_STORE_WARNING.format(hostname=session.bound_host, session_id=session.session_id)
|
||||
if not session.is_anonymous:
|
||||
warning += OLD_HEADER_STORE_WARNING_FOR_NAMED_SESSIONS
|
||||
warning += OLD_HEADER_STORE_LINK
|
||||
session.warn_legacy_usage(warning)
|
||||
|
||||
return normalized_headers
|
||||
|
||||
|
||||
def post_process(
|
||||
normalized_headers: List[Dict[str, Any]],
|
||||
*,
|
||||
original_type: Type[Any]
|
||||
) -> Any:
|
||||
"""Deserialize given header store into the original form it was
|
||||
used in."""
|
||||
|
||||
if issubclass(original_type, dict):
|
||||
# For the legacy behavior, preserve the last value.
|
||||
return {
|
||||
item['name']: item['value']
|
||||
for item in normalized_headers
|
||||
}
|
||||
else:
|
||||
return normalized_headers
|
||||
|
||||
|
||||
def fix_layout(session: 'Session', *args, **kwargs) -> None:
|
||||
from httpie.sessions import materialize_headers
|
||||
|
||||
if not isinstance(session['headers'], dict):
|
||||
return None
|
||||
|
||||
session['headers'] = materialize_headers(session['headers'])
|
@ -37,7 +37,8 @@ def main(args: List[Union[str, bytes]] = sys.argv, env: Environment = Environmen
|
||||
parser=parser,
|
||||
main_program=main_program,
|
||||
args=args,
|
||||
env=env
|
||||
env=env,
|
||||
use_default_options=False,
|
||||
)
|
||||
except argparse.ArgumentError:
|
||||
program_args = args[1:]
|
||||
|
@ -1,39 +1,94 @@
|
||||
from textwrap import dedent
|
||||
from httpie.cli.argparser import HTTPieManagerArgumentParser
|
||||
from httpie.cli.options import Qualifiers, ARGPARSE_QUALIFIER_MAP, map_qualifiers, parser_to_parser_spec
|
||||
from httpie import __version__
|
||||
|
||||
CLI_SESSION_UPGRADE_FLAGS = [
|
||||
{
|
||||
'flags': ['--bind-cookies'],
|
||||
'action': 'store_true',
|
||||
'default': False,
|
||||
'help': 'Bind domainless cookies to the host that session belongs.'
|
||||
}
|
||||
]
|
||||
|
||||
COMMANDS = {
|
||||
'plugins': {
|
||||
'help': 'Manage HTTPie plugins.',
|
||||
'install': [
|
||||
'Install the given targets from PyPI '
|
||||
'or from a local paths.',
|
||||
'cli': {
|
||||
'help': 'Manage HTTPie for Terminal',
|
||||
'export-args': [
|
||||
'Export available options for the CLI',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to install'
|
||||
'flags': ['-f', '--format'],
|
||||
'choices': ['json'],
|
||||
'help': 'Format to export in.',
|
||||
'default': 'json'
|
||||
}
|
||||
],
|
||||
'upgrade': [
|
||||
'Upgrade the given plugins',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to upgrade'
|
||||
}
|
||||
'check-updates': [
|
||||
'Check for updates'
|
||||
],
|
||||
'uninstall': [
|
||||
'Uninstall the given HTTPie plugins.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
'list': [
|
||||
'List all installed HTTPie plugins.'
|
||||
],
|
||||
},
|
||||
'sessions': {
|
||||
'help': 'Manage HTTPie 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.',
|
||||
{
|
||||
'dest': 'hostname',
|
||||
'metavar': 'HOSTNAME',
|
||||
'help': 'The host this session belongs.'
|
||||
},
|
||||
{
|
||||
'dest': 'session',
|
||||
'metavar': 'SESSION_NAME_OR_PATH',
|
||||
'help': 'The name or the path for the session that will be upgraded.'
|
||||
},
|
||||
*CLI_SESSION_UPGRADE_FLAGS
|
||||
],
|
||||
'upgrade-all': [
|
||||
'Upgrade all named sessions with the latest layout. A list of '
|
||||
'changes between different session versions can be found in the official '
|
||||
'documentation.',
|
||||
*CLI_SESSION_UPGRADE_FLAGS
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
||||
'help': 'Manage HTTPie plugins.',
|
||||
'install': [
|
||||
'Install the given targets from PyPI '
|
||||
'or from a local paths.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
'upgrade': [
|
||||
'Upgrade the given plugins',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to upgrade'
|
||||
}
|
||||
],
|
||||
'uninstall': [
|
||||
'Uninstall the given HTTPie plugins.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
'list': [
|
||||
'List all installed HTTPie plugins.'
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@ -47,22 +102,28 @@ def missing_subcommand(*args) -> str:
|
||||
return f'Please specify one of these: {subcommands}'
|
||||
|
||||
|
||||
def generate_subparsers(root, parent_parser, definitions):
|
||||
def generate_subparsers(root, parent_parser, definitions, spec):
|
||||
action_dest = '_'.join(parent_parser.prog.split()[1:] + ['action'])
|
||||
actions = parent_parser.add_subparsers(
|
||||
dest=action_dest
|
||||
)
|
||||
for command, properties in definitions.items():
|
||||
is_subparser = isinstance(properties, dict)
|
||||
properties = properties.copy()
|
||||
|
||||
descr = properties.pop('help', None) if is_subparser else properties.pop(0)
|
||||
command_parser = actions.add_parser(command, description=descr)
|
||||
command_parser.root = root
|
||||
if is_subparser:
|
||||
generate_subparsers(root, command_parser, properties)
|
||||
generate_subparsers(root, command_parser, properties, spec)
|
||||
continue
|
||||
|
||||
group = spec.add_group(parent_parser.prog + ' ' + command, description=descr)
|
||||
for argument in properties:
|
||||
command_parser.add_argument(**argument)
|
||||
argument = argument.copy()
|
||||
flags = argument.pop('flags', [])
|
||||
command_parser.add_argument(*flags, **map_qualifiers(argument, ARGPARSE_QUALIFIER_MAP))
|
||||
group.add_argument(*flags, **argument)
|
||||
|
||||
|
||||
parser = HTTPieManagerArgumentParser(
|
||||
@ -109,4 +170,12 @@ parser.add_argument(
|
||||
'''
|
||||
)
|
||||
|
||||
generate_subparsers(parser, parser, COMMANDS)
|
||||
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)
|
||||
|
69
httpie/manager/compat.py
Normal file
69
httpie/manager/compat.py
Normal file
@ -0,0 +1,69 @@
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from contextlib import suppress
|
||||
from typing import List, Optional
|
||||
from httpie.compat import is_frozen
|
||||
|
||||
|
||||
class PipError(Exception):
|
||||
"""An exception that occurs when pip exits with an error status code."""
|
||||
|
||||
def __init__(self, stdout, stderr):
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def _discover_system_pip() -> List[str]:
|
||||
# When we are running inside of a frozen binary, we need the system
|
||||
# pip to install plugins since there is no way for us to execute any
|
||||
# code outside of the HTTPie.
|
||||
#
|
||||
# We explicitly depend on system pip, so the SystemError should not
|
||||
# be executed (except for broken installations).
|
||||
def _check_pip_version(pip_location: Optional[str]) -> bool:
|
||||
if not pip_location:
|
||||
return False
|
||||
|
||||
with suppress(subprocess.CalledProcessError):
|
||||
stdout = subprocess.check_output([pip_location, "--version"], text=True)
|
||||
return "python 3" in stdout
|
||||
|
||||
targets = [
|
||||
"pip",
|
||||
"pip3"
|
||||
]
|
||||
for target in targets:
|
||||
pip_location = shutil.which(target)
|
||||
if _check_pip_version(pip_location):
|
||||
return pip_location
|
||||
|
||||
raise SystemError("Couldn't find 'pip' executable. Please ensure that pip in your system is available.")
|
||||
|
||||
|
||||
def _run_pip_subprocess(pip_executable: List[str], args: List[str]) -> bytes:
|
||||
import subprocess
|
||||
|
||||
cmd = [*pip_executable, *args]
|
||||
try:
|
||||
process = subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
shell=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
raise PipError(error.stdout, error.stderr) from error
|
||||
else:
|
||||
return process.stdout
|
||||
|
||||
|
||||
def run_pip(args: List[str]) -> bytes:
|
||||
if is_frozen:
|
||||
pip_executable = [_discover_system_pip()]
|
||||
else:
|
||||
pip_executable = [sys.executable, '-m', 'pip']
|
||||
|
||||
return _run_pip_subprocess(pip_executable, args)
|
@ -1,9 +1,10 @@
|
||||
import argparse
|
||||
from typing import Optional
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.manager.plugins import PluginInstaller
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.manager.cli import missing_subcommand, parser
|
||||
from httpie.manager.tasks import CLI_TASKS
|
||||
|
||||
MSG_COMMAND_CONFUSION = '''\
|
||||
This command is only for managing HTTPie plugins.
|
||||
@ -22,12 +23,20 @@ MSG_NAKED_INVOCATION = f'''\
|
||||
'''.rstrip("\n").format(args='POST pie.dev/post hello=world')
|
||||
|
||||
|
||||
def dispatch_cli_task(env: Environment, action: Optional[str], args: argparse.Namespace) -> ExitStatus:
|
||||
if action is None:
|
||||
parser.error(missing_subcommand('cli'))
|
||||
|
||||
return CLI_TASKS[action](env, args)
|
||||
|
||||
|
||||
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
if args.action is None:
|
||||
parser.error(MSG_NAKED_INVOCATION)
|
||||
|
||||
if args.action == 'plugins':
|
||||
plugins = PluginInstaller(env, debug=args.debug)
|
||||
return plugins.run(args.plugins_action, args)
|
||||
return dispatch_cli_task(env, args.action, args)
|
||||
elif args.action == 'cli':
|
||||
return dispatch_cli_task(env, args.cli_action, args)
|
||||
|
||||
return ExitStatus.SUCCESS
|
||||
|
11
httpie/manager/tasks/__init__.py
Normal file
11
httpie/manager/tasks/__init__.py
Normal file
@ -0,0 +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
|
27
httpie/manager/tasks/export_args.py
Normal file
27
httpie/manager/tasks/export_args.py
Normal file
@ -0,0 +1,27 @@
|
||||
import argparse
|
||||
import json
|
||||
|
||||
from httpie.cli.definition import options
|
||||
from httpie.cli.options import to_data
|
||||
from httpie.output.writer import write_raw_data
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.context import Environment
|
||||
|
||||
|
||||
FORMAT_TO_CONTENT_TYPE = {
|
||||
'json': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
def cli_export_args(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
if args.format == 'json':
|
||||
data = json.dumps(to_data(options))
|
||||
else:
|
||||
raise NotImplementedError(f'Unexpected format value: {args.format}')
|
||||
|
||||
write_raw_data(
|
||||
env,
|
||||
data,
|
||||
stream_kwargs={'mime_overwrite': FORMAT_TO_CONTENT_TYPE[args.format]},
|
||||
)
|
||||
return ExitStatus.SUCCESS
|
@ -1,20 +1,19 @@
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
import re
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Tuple, Optional, List
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from httpie.manager.compat import PipError, run_pip
|
||||
from httpie.manager.cli import parser, missing_subcommand
|
||||
from httpie.compat import importlib_metadata, get_dist_name
|
||||
from httpie.compat import get_dist_name, importlib_metadata
|
||||
from httpie.context import Environment
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.utils import as_site
|
||||
from httpie.utils import get_site_paths
|
||||
|
||||
PEP_503 = re.compile(r"[-_.]+")
|
||||
|
||||
@ -58,46 +57,37 @@ class PluginInstaller:
|
||||
self.env.stderr.write(message + '\n')
|
||||
return ExitStatus.ERROR
|
||||
|
||||
def pip(self, *args, **kwargs) -> subprocess.CompletedProcess:
|
||||
options = {
|
||||
'check': True,
|
||||
'shell': False,
|
||||
'stdout': self.env.stdout,
|
||||
'stderr': subprocess.PIPE,
|
||||
}
|
||||
options.update(kwargs)
|
||||
|
||||
cmd = [sys.executable, '-m', 'pip', *args]
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
**options
|
||||
)
|
||||
|
||||
def _install(self, targets: List[str], mode='install', **process_options) -> Tuple[
|
||||
Optional[bytes], ExitStatus
|
||||
def _install(self, targets: List[str], mode='install') -> Tuple[
|
||||
bytes, ExitStatus
|
||||
]:
|
||||
pip_args = [
|
||||
'install',
|
||||
'--prefer-binary',
|
||||
f'--prefix={self.dir}',
|
||||
'--no-warn-script-location',
|
||||
]
|
||||
if mode == 'upgrade':
|
||||
pip_args.append('--upgrade')
|
||||
pip_args.extend(targets)
|
||||
|
||||
try:
|
||||
process = self.pip(
|
||||
*pip_args,
|
||||
*targets,
|
||||
**process_options,
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
stdout = run_pip(pip_args)
|
||||
except PipError as pip_error:
|
||||
error = pip_error
|
||||
stdout = pip_error.stdout
|
||||
else:
|
||||
error = None
|
||||
|
||||
self.env.stdout.write(stdout.decode())
|
||||
|
||||
if error:
|
||||
reason = None
|
||||
if error.stderr:
|
||||
stderr = error.stderr.decode()
|
||||
|
||||
if self.debug:
|
||||
self.env.stderr.write('Command failed: ')
|
||||
self.env.stderr.write(' '.join(error.cmd) + '\n')
|
||||
self.env.stderr.write('pip ' + ' '.join(pip_args) + '\n')
|
||||
self.env.stderr.write(textwrap.indent(' ', stderr))
|
||||
|
||||
last_line = stderr.strip().splitlines()[-1]
|
||||
@ -108,7 +98,6 @@ class PluginInstaller:
|
||||
stdout = error.stdout
|
||||
exit_status = self.fail(mode, ', '.join(targets), reason)
|
||||
else:
|
||||
stdout = process.stdout
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
|
||||
return stdout, exit_status
|
||||
@ -124,10 +113,11 @@ class PluginInstaller:
|
||||
# existing metadata for old versions manually.
|
||||
# [0]: https://github.com/pypa/pip/issues/10727
|
||||
result_deps = defaultdict(list)
|
||||
for child in as_site(self.dir).iterdir():
|
||||
if child.suffix in {'.dist-info', '.egg-info'}:
|
||||
name, _, version = child.stem.rpartition('-')
|
||||
result_deps[name].append((version, child))
|
||||
for site_dir in get_site_paths(self.dir):
|
||||
for child in site_dir.iterdir():
|
||||
if child.suffix in {'.dist-info', '.egg-info'}:
|
||||
name, _, version = child.stem.rpartition('-')
|
||||
result_deps[name].append((version, child))
|
||||
|
||||
for target in targets:
|
||||
name, _, version = target.rpartition('-')
|
||||
@ -145,15 +135,12 @@ class PluginInstaller:
|
||||
|
||||
raw_stdout, exit_status = self._install(
|
||||
targets,
|
||||
mode='upgrade',
|
||||
stdout=subprocess.PIPE
|
||||
mode='upgrade'
|
||||
)
|
||||
if not raw_stdout:
|
||||
return exit_status
|
||||
|
||||
stdout = raw_stdout.decode()
|
||||
self.env.stdout.write(stdout)
|
||||
|
||||
installation_line = stdout.splitlines()[-1]
|
||||
if installation_line.startswith('Successfully installed'):
|
||||
self._clear_metadata(installation_line.split()[2:])
|
||||
@ -178,7 +165,7 @@ class PluginInstaller:
|
||||
return self.fail('uninstall', target, 'couldn\'t locate the package')
|
||||
|
||||
# TODO: Consider handling failures here (e.g if it fails,
|
||||
# just rever the operation and leave the site-packages
|
||||
# just revert the operation and leave the site-packages
|
||||
# in a proper shape).
|
||||
for file in files:
|
||||
with suppress(FileNotFoundError):
|
||||
@ -248,3 +235,14 @@ class PluginInstaller:
|
||||
status = self.list()
|
||||
|
||||
return status or ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def cli_plugins(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
plugins = PluginInstaller(env, debug=args.debug)
|
||||
|
||||
try:
|
||||
action = args.cli_plugins_action
|
||||
except AttributeError:
|
||||
action = args.plugins_action
|
||||
|
||||
return plugins.run(action, args)
|
86
httpie/manager/tasks/sessions.py
Normal file
86
httpie/manager/tasks/sessions.py
Normal file
@ -0,0 +1,86 @@
|
||||
import argparse
|
||||
|
||||
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 = {
|
||||
'3.1.0': v3_1_0_session_cookie_format.fix_layout,
|
||||
'3.2.0': v3_2_0_session_header_format.fix_layout,
|
||||
}
|
||||
|
||||
|
||||
def cli_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
action = args.cli_sessions_action
|
||||
if action is None:
|
||||
parser.error(missing_subcommand('cli', 'sessions'))
|
||||
|
||||
if action == 'upgrade':
|
||||
return cli_upgrade_session(env, args)
|
||||
elif action == 'upgrade-all':
|
||||
return cli_upgrade_all_sessions(env, args)
|
||||
else:
|
||||
raise ValueError(f'Unexpected action: {action}')
|
||||
|
||||
|
||||
def upgrade_session(env: Environment, args: argparse.Namespace, hostname: str, session_name: str):
|
||||
session = get_httpie_session(
|
||||
env=env,
|
||||
config_dir=env.config.directory,
|
||||
session_name=session_name,
|
||||
host=hostname,
|
||||
url=hostname,
|
||||
suppress_legacy_warnings=True
|
||||
)
|
||||
|
||||
session_name = session.path.stem
|
||||
if session.is_new():
|
||||
env.log_error(f'{session_name!r} @ {hostname!r} does not exist.')
|
||||
return ExitStatus.ERROR
|
||||
|
||||
fixers = [
|
||||
fixer
|
||||
for version, fixer in FIXERS_TO_VERSIONS.items()
|
||||
if is_version_greater(version, session.version)
|
||||
]
|
||||
|
||||
if len(fixers) == 0:
|
||||
env.stdout.write(f'{session_name!r} @ {hostname!r} is already up to date.\n')
|
||||
return ExitStatus.SUCCESS
|
||||
|
||||
for fixer in fixers:
|
||||
fixer(session, hostname, args)
|
||||
|
||||
session.save(bump_version=True)
|
||||
env.stdout.write(f'Upgraded {session_name!r} @ {hostname!r} to v{session.version}\n')
|
||||
return ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def cli_upgrade_session(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
return upgrade_session(
|
||||
env,
|
||||
args=args,
|
||||
hostname=args.hostname,
|
||||
session_name=args.session
|
||||
)
|
||||
|
||||
|
||||
def cli_upgrade_all_sessions(env: Environment, args: argparse.Namespace) -> ExitStatus:
|
||||
session_dir_path = env.config_dir / SESSIONS_DIR_NAME
|
||||
|
||||
status = ExitStatus.SUCCESS
|
||||
for host_path in session_dir_path.iterdir():
|
||||
hostname = host_path.name
|
||||
for session_path in host_path.glob("*.json"):
|
||||
session_name = session_path.stem
|
||||
status |= upgrade_session(
|
||||
env,
|
||||
args=args,
|
||||
hostname=hostname,
|
||||
session_name=session_name
|
||||
)
|
||||
return status
|
@ -1,3 +1,5 @@
|
||||
from time import monotonic
|
||||
|
||||
import requests
|
||||
|
||||
from enum import Enum, auto
|
||||
@ -15,6 +17,9 @@ from .compat import cached_property
|
||||
from .utils import split_cookies, parse_content_type_header
|
||||
|
||||
|
||||
ELAPSED_TIME_LABEL = 'Elapsed time'
|
||||
|
||||
|
||||
class HTTPMessage:
|
||||
"""Abstract class for HTTP messages."""
|
||||
|
||||
@ -66,7 +71,11 @@ class HTTPResponse(HTTPMessage):
|
||||
@property
|
||||
def headers(self):
|
||||
try:
|
||||
raw_version = self._orig.raw._original_response.version
|
||||
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
|
||||
@ -74,7 +83,7 @@ class HTTPResponse(HTTPMessage):
|
||||
9: '0.9',
|
||||
10: '1.0',
|
||||
11: '1.1',
|
||||
20: '2',
|
||||
20: '2.0',
|
||||
}[raw_version]
|
||||
|
||||
original = self._orig
|
||||
@ -96,7 +105,13 @@ class HTTPResponse(HTTPMessage):
|
||||
@property
|
||||
def metadata(self) -> str:
|
||||
data = {}
|
||||
data['Elapsed time'] = str(self._orig.elapsed.total_seconds()) + 's'
|
||||
time_to_parse_headers = self._orig.elapsed.total_seconds()
|
||||
# noinspection PyProtectedMember
|
||||
time_since_headers_parsed = monotonic() - self._orig._httpie_headers_parsed_at
|
||||
time_elapsed = time_to_parse_headers + time_since_headers_parsed
|
||||
# data['Headers time'] = str(round(time_to_parse_headers, 5)) + 's'
|
||||
# data['Body time'] = str(round(time_since_headers_parsed, 5)) + 's'
|
||||
data[ELAPSED_TIME_LABEL] = str(round(time_elapsed, 10)) + 's'
|
||||
return '\n'.join(
|
||||
f'{key}: {value}'
|
||||
for key, value in data.items()
|
||||
|
@ -17,14 +17,15 @@ from pygments.util import ClassNotFound
|
||||
|
||||
from ..lexers.json import EnhancedJsonLexer
|
||||
from ..lexers.metadata import MetadataLexer
|
||||
from ..ui.palette import SHADE_NAMES, get_color
|
||||
from ..ui.palette import AUTO_STYLE, SHADE_TO_PIE_STYLE, PieColor, ColorString, get_color
|
||||
from ...context import Environment
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
DEFAULT_STYLE = AUTO_STYLE
|
||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
||||
PYGMENTS_BOLD = ColorString('bold')
|
||||
PYGMENTS_ITALIC = ColorString('italic')
|
||||
|
||||
BUNDLED_STYLES = {
|
||||
SOLARIZED_STYLE,
|
||||
@ -33,7 +34,7 @@ BUNDLED_STYLES = {
|
||||
|
||||
|
||||
def get_available_styles():
|
||||
return BUNDLED_STYLES | set(pygments.styles.get_all_styles())
|
||||
return sorted(BUNDLED_STYLES | set(pygments.styles.get_all_styles()))
|
||||
|
||||
|
||||
class ColorFormatter(FormatterPlugin):
|
||||
@ -254,11 +255,11 @@ class Solarized256Style(pygments.style.Style):
|
||||
pygments.token.Comment.Preproc: GREEN,
|
||||
pygments.token.Comment.Special: GREEN,
|
||||
pygments.token.Generic.Deleted: CYAN,
|
||||
pygments.token.Generic.Emph: 'italic',
|
||||
pygments.token.Generic.Emph: PYGMENTS_ITALIC,
|
||||
pygments.token.Generic.Error: RED,
|
||||
pygments.token.Generic.Heading: ORANGE,
|
||||
pygments.token.Generic.Inserted: GREEN,
|
||||
pygments.token.Generic.Strong: 'bold',
|
||||
pygments.token.Generic.Strong: PYGMENTS_BOLD,
|
||||
pygments.token.Generic.Subheading: ORANGE,
|
||||
pygments.token.Token: BASE1,
|
||||
pygments.token.Token.Other: ORANGE,
|
||||
@ -267,86 +268,86 @@ class Solarized256Style(pygments.style.Style):
|
||||
|
||||
PIE_HEADER_STYLE = {
|
||||
# HTTP line / Headers / Etc.
|
||||
pygments.token.Name.Namespace: 'bold primary',
|
||||
pygments.token.Keyword.Reserved: 'bold grey',
|
||||
pygments.token.Operator: 'bold grey',
|
||||
pygments.token.Number: 'bold grey',
|
||||
pygments.token.Name.Function.Magic: 'bold green',
|
||||
pygments.token.Name.Exception: 'bold green',
|
||||
pygments.token.Name.Attribute: 'blue',
|
||||
pygments.token.String: 'primary',
|
||||
pygments.token.Name.Namespace: PYGMENTS_BOLD | PieColor.PRIMARY,
|
||||
pygments.token.Keyword.Reserved: PYGMENTS_BOLD | PieColor.GREY,
|
||||
pygments.token.Operator: PYGMENTS_BOLD | PieColor.GREY,
|
||||
pygments.token.Number: PYGMENTS_BOLD | PieColor.GREY,
|
||||
pygments.token.Name.Function.Magic: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Name.Exception: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Name.Attribute: PieColor.BLUE,
|
||||
pygments.token.String: PieColor.PRIMARY,
|
||||
|
||||
# HTTP Methods
|
||||
pygments.token.Name.Function: 'bold grey',
|
||||
pygments.token.Name.Function.HTTP.GET: 'bold green',
|
||||
pygments.token.Name.Function.HTTP.HEAD: 'bold green',
|
||||
pygments.token.Name.Function.HTTP.POST: 'bold yellow',
|
||||
pygments.token.Name.Function.HTTP.PUT: 'bold orange',
|
||||
pygments.token.Name.Function.HTTP.PATCH: 'bold orange',
|
||||
pygments.token.Name.Function.HTTP.DELETE: 'bold red',
|
||||
pygments.token.Name.Function: PYGMENTS_BOLD | PieColor.GREY,
|
||||
pygments.token.Name.Function.HTTP.GET: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Name.Function.HTTP.HEAD: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Name.Function.HTTP.POST: PYGMENTS_BOLD | PieColor.YELLOW,
|
||||
pygments.token.Name.Function.HTTP.PUT: PYGMENTS_BOLD | PieColor.ORANGE,
|
||||
pygments.token.Name.Function.HTTP.PATCH: PYGMENTS_BOLD | PieColor.ORANGE,
|
||||
pygments.token.Name.Function.HTTP.DELETE: PYGMENTS_BOLD | PieColor.RED,
|
||||
|
||||
# HTTP status codes
|
||||
pygments.token.Number.HTTP.INFO: 'bold aqua',
|
||||
pygments.token.Number.HTTP.OK: 'bold green',
|
||||
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
|
||||
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
|
||||
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
|
||||
pygments.token.Number.HTTP.INFO: PYGMENTS_BOLD | PieColor.AQUA,
|
||||
pygments.token.Number.HTTP.OK: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Number.HTTP.REDIRECT: PYGMENTS_BOLD | PieColor.YELLOW,
|
||||
pygments.token.Number.HTTP.CLIENT_ERR: PYGMENTS_BOLD | PieColor.ORANGE,
|
||||
pygments.token.Number.HTTP.SERVER_ERR: PYGMENTS_BOLD | PieColor.RED,
|
||||
|
||||
# Metadata
|
||||
pygments.token.Name.Decorator: 'grey',
|
||||
pygments.token.Number.SPEED.FAST: 'bold green',
|
||||
pygments.token.Number.SPEED.AVG: 'bold yellow',
|
||||
pygments.token.Number.SPEED.SLOW: 'bold orange',
|
||||
pygments.token.Number.SPEED.VERY_SLOW: 'bold red',
|
||||
pygments.token.Name.Decorator: PieColor.GREY,
|
||||
pygments.token.Number.SPEED.FAST: PYGMENTS_BOLD | PieColor.GREEN,
|
||||
pygments.token.Number.SPEED.AVG: PYGMENTS_BOLD | PieColor.YELLOW,
|
||||
pygments.token.Number.SPEED.SLOW: PYGMENTS_BOLD | PieColor.ORANGE,
|
||||
pygments.token.Number.SPEED.VERY_SLOW: PYGMENTS_BOLD | PieColor.RED,
|
||||
}
|
||||
|
||||
PIE_BODY_STYLE = {
|
||||
# {}[]:
|
||||
pygments.token.Punctuation: 'grey',
|
||||
pygments.token.Punctuation: PieColor.GREY,
|
||||
|
||||
# Keys
|
||||
pygments.token.Name.Tag: 'pink',
|
||||
pygments.token.Name.Tag: PieColor.PINK,
|
||||
|
||||
# Values
|
||||
pygments.token.Literal.String: 'green',
|
||||
pygments.token.Literal.String.Double: 'green',
|
||||
pygments.token.Literal.Number: 'aqua',
|
||||
pygments.token.Keyword: 'orange',
|
||||
pygments.token.Literal.String: PieColor.GREEN,
|
||||
pygments.token.Literal.String.Double: PieColor.GREEN,
|
||||
pygments.token.Literal.Number: PieColor.AQUA,
|
||||
pygments.token.Keyword: PieColor.ORANGE,
|
||||
|
||||
# Other stuff
|
||||
pygments.token.Text: 'primary',
|
||||
pygments.token.Name.Attribute: 'primary',
|
||||
pygments.token.Name.Builtin: 'blue',
|
||||
pygments.token.Name.Builtin.Pseudo: 'blue',
|
||||
pygments.token.Name.Class: 'blue',
|
||||
pygments.token.Name.Constant: 'orange',
|
||||
pygments.token.Name.Decorator: 'blue',
|
||||
pygments.token.Name.Entity: 'orange',
|
||||
pygments.token.Name.Exception: 'yellow',
|
||||
pygments.token.Name.Function: 'blue',
|
||||
pygments.token.Name.Variable: 'blue',
|
||||
pygments.token.String: 'aqua',
|
||||
pygments.token.String.Backtick: 'secondary',
|
||||
pygments.token.String.Char: 'aqua',
|
||||
pygments.token.String.Doc: 'aqua',
|
||||
pygments.token.String.Escape: 'red',
|
||||
pygments.token.String.Heredoc: 'aqua',
|
||||
pygments.token.String.Regex: 'red',
|
||||
pygments.token.Number: 'aqua',
|
||||
pygments.token.Operator: 'primary',
|
||||
pygments.token.Operator.Word: 'green',
|
||||
pygments.token.Comment: 'secondary',
|
||||
pygments.token.Comment.Preproc: 'green',
|
||||
pygments.token.Comment.Special: 'green',
|
||||
pygments.token.Generic.Deleted: 'aqua',
|
||||
pygments.token.Generic.Emph: 'italic',
|
||||
pygments.token.Generic.Error: 'red',
|
||||
pygments.token.Generic.Heading: 'orange',
|
||||
pygments.token.Generic.Inserted: 'green',
|
||||
pygments.token.Generic.Strong: 'bold',
|
||||
pygments.token.Generic.Subheading: 'orange',
|
||||
pygments.token.Token: 'primary',
|
||||
pygments.token.Token.Other: 'orange',
|
||||
pygments.token.Text: PieColor.PRIMARY,
|
||||
pygments.token.Name.Attribute: PieColor.PRIMARY,
|
||||
pygments.token.Name.Builtin: PieColor.BLUE,
|
||||
pygments.token.Name.Builtin.Pseudo: PieColor.BLUE,
|
||||
pygments.token.Name.Class: PieColor.BLUE,
|
||||
pygments.token.Name.Constant: PieColor.ORANGE,
|
||||
pygments.token.Name.Decorator: PieColor.BLUE,
|
||||
pygments.token.Name.Entity: PieColor.ORANGE,
|
||||
pygments.token.Name.Exception: PieColor.YELLOW,
|
||||
pygments.token.Name.Function: PieColor.BLUE,
|
||||
pygments.token.Name.Variable: PieColor.BLUE,
|
||||
pygments.token.String: PieColor.AQUA,
|
||||
pygments.token.String.Backtick: PieColor.SECONDARY,
|
||||
pygments.token.String.Char: PieColor.AQUA,
|
||||
pygments.token.String.Doc: PieColor.AQUA,
|
||||
pygments.token.String.Escape: PieColor.RED,
|
||||
pygments.token.String.Heredoc: PieColor.AQUA,
|
||||
pygments.token.String.Regex: PieColor.RED,
|
||||
pygments.token.Number: PieColor.AQUA,
|
||||
pygments.token.Operator: PieColor.PRIMARY,
|
||||
pygments.token.Operator.Word: PieColor.GREEN,
|
||||
pygments.token.Comment: PieColor.SECONDARY,
|
||||
pygments.token.Comment.Preproc: PieColor.GREEN,
|
||||
pygments.token.Comment.Special: PieColor.GREEN,
|
||||
pygments.token.Generic.Deleted: PieColor.AQUA,
|
||||
pygments.token.Generic.Emph: PYGMENTS_ITALIC,
|
||||
pygments.token.Generic.Error: PieColor.RED,
|
||||
pygments.token.Generic.Heading: PieColor.ORANGE,
|
||||
pygments.token.Generic.Inserted: PieColor.GREEN,
|
||||
pygments.token.Generic.Strong: PYGMENTS_BOLD,
|
||||
pygments.token.Generic.Subheading: PieColor.ORANGE,
|
||||
pygments.token.Token: PieColor.PRIMARY,
|
||||
pygments.token.Token.Other: PieColor.ORANGE,
|
||||
}
|
||||
|
||||
|
||||
@ -370,7 +371,7 @@ def make_style(name, raw_styles, shade):
|
||||
def make_styles():
|
||||
styles = {}
|
||||
|
||||
for shade, name in SHADE_NAMES.items():
|
||||
for shade, name in SHADE_TO_PIE_STYLE.items():
|
||||
styles[name] = [
|
||||
make_style(name, style_map, shade)
|
||||
for style_name, style_map in [
|
||||
@ -383,4 +384,5 @@ def make_styles():
|
||||
|
||||
|
||||
PIE_STYLES = make_styles()
|
||||
PIE_STYLE_NAMES = list(PIE_STYLES.keys())
|
||||
BUNDLED_STYLES |= PIE_STYLES.keys()
|
||||
|
@ -2,7 +2,7 @@ import re
|
||||
import pygments
|
||||
from httpie.output.lexers.common import precise
|
||||
|
||||
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
|
||||
RE_STATUS_LINE = re.compile(r'(\d{3})( +)?(.+)?')
|
||||
|
||||
STATUS_TYPES = {
|
||||
'1': pygments.token.Number.HTTP.INFO,
|
||||
|
@ -1,4 +1,6 @@
|
||||
import pygments
|
||||
|
||||
from httpie.models import ELAPSED_TIME_LABEL
|
||||
from httpie.output.lexers.common import precise
|
||||
|
||||
SPEED_TOKENS = {
|
||||
@ -34,7 +36,7 @@ class MetadataLexer(pygments.lexer.RegexLexer):
|
||||
tokens = {
|
||||
'root': [
|
||||
(
|
||||
r'(Elapsed time)( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
|
||||
fr'({ELAPSED_TIME_LABEL})( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Decorator, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
|
44
httpie/output/models.py
Normal file
44
httpie/output/models.py
Normal file
@ -0,0 +1,44 @@
|
||||
import argparse
|
||||
from typing import Any, Dict, Union, List, NamedTuple, Optional
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.cli.constants import PrettyOptions, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY
|
||||
from httpie.cli.argtypes import PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
from httpie.output.formatters.colors import AUTO_STYLE
|
||||
|
||||
|
||||
class ProcessingOptions(NamedTuple):
|
||||
"""Represents a set of stylistic options
|
||||
that are used when deciding which stream
|
||||
should be used."""
|
||||
|
||||
debug: bool = False
|
||||
traceback: bool = False
|
||||
|
||||
stream: bool = False
|
||||
style: str = AUTO_STYLE
|
||||
prettify: Union[List[str], PrettyOptions] = PRETTY_STDOUT_TTY_ONLY
|
||||
|
||||
response_mime: Optional[str] = None
|
||||
response_charset: Optional[str] = None
|
||||
|
||||
json: bool = False
|
||||
format_options: Dict[str, Any] = PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
|
||||
def get_prettify(self, env: Environment) -> List[str]:
|
||||
if self.prettify is PRETTY_STDOUT_TTY_ONLY:
|
||||
return PRETTY_MAP['all' if env.stdout_isatty else 'none']
|
||||
else:
|
||||
return self.prettify
|
||||
|
||||
@classmethod
|
||||
def from_raw_args(cls, options: argparse.Namespace) -> 'ProcessingOptions':
|
||||
fetched_options = {
|
||||
option: getattr(options, option)
|
||||
for option in cls._fields
|
||||
}
|
||||
return cls(**fetched_options)
|
||||
|
||||
@property
|
||||
def show_traceback(self):
|
||||
return self.debug or self.traceback
|
@ -34,7 +34,8 @@ class BaseStream(metaclass=ABCMeta):
|
||||
self,
|
||||
msg: HTTPMessage,
|
||||
output_options: OutputOptions,
|
||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
||||
on_body_chunk_downloaded: Callable[[bytes], None] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
:param msg: a :class:`models.HTTPMessage` subclass
|
||||
@ -45,6 +46,7 @@ class BaseStream(metaclass=ABCMeta):
|
||||
self.msg = msg
|
||||
self.output_options = output_options
|
||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||
self.extra_options = kwargs
|
||||
|
||||
def get_headers(self) -> bytes:
|
||||
"""Return the headers' bytes."""
|
||||
|
47
httpie/output/ui/man_pages.py
Normal file
47
httpie/output/ui/man_pages.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""Logic for checking and displaying man pages."""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
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."""
|
||||
|
||||
if NO_MAN_PAGES or os.system == 'nt':
|
||||
return False
|
||||
|
||||
try:
|
||||
process = subprocess.run(
|
||||
[MAN_COMMAND, MAN_PAGE_SECTION, program],
|
||||
shell=False,
|
||||
stdout=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
|
||||
|
||||
|
||||
def display_for(env: Environment, program: str) -> None:
|
||||
"""Display the man page for the given command (http/https)."""
|
||||
|
||||
subprocess.run(
|
||||
[MAN_COMMAND, MAN_PAGE_SECTION, program],
|
||||
stdout=env.stdout,
|
||||
stderr=env.stderr
|
||||
)
|
@ -1,12 +1,118 @@
|
||||
# Copy the brand palette
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
PYGMENTS_BRIGHT_BLACK = 'ansibrightblack'
|
||||
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
|
||||
|
||||
class Styles(Enum):
|
||||
PIE = auto()
|
||||
ANSI = auto()
|
||||
|
||||
|
||||
class PieStyle(str, Enum):
|
||||
UNIVERSAL = 'pie'
|
||||
DARK = 'pie-dark'
|
||||
LIGHT = 'pie-light'
|
||||
|
||||
|
||||
PIE_STYLE_TO_SHADE = {
|
||||
PieStyle.DARK: '500',
|
||||
PieStyle.UNIVERSAL: '600',
|
||||
PieStyle.LIGHT: '700',
|
||||
}
|
||||
SHADE_TO_PIE_STYLE = {
|
||||
shade: style for style, shade in PIE_STYLE_TO_SHADE.items()
|
||||
}
|
||||
|
||||
|
||||
class ColorString(str):
|
||||
def __or__(self, other: str) -> 'ColorString':
|
||||
"""Combine a style with a property.
|
||||
|
||||
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):
|
||||
"""Styles that are available only in Pie themes."""
|
||||
|
||||
PRIMARY = 'primary'
|
||||
SECONDARY = 'secondary'
|
||||
|
||||
WHITE = 'white'
|
||||
BLACK = 'black'
|
||||
GREY = 'grey'
|
||||
AQUA = 'aqua'
|
||||
PURPLE = 'purple'
|
||||
ORANGE = 'orange'
|
||||
RED = 'red'
|
||||
BLUE = 'blue'
|
||||
PINK = 'pink'
|
||||
GREEN = 'green'
|
||||
YELLOW = 'yellow'
|
||||
|
||||
|
||||
class GenericColor(Enum):
|
||||
"""Generic colors that are safe to use everywhere."""
|
||||
|
||||
# <https://rich.readthedocs.io/en/stable/appendix/colors.html>
|
||||
|
||||
WHITE = {Styles.PIE: PieColor.WHITE, Styles.ANSI: 'white'}
|
||||
BLACK = {Styles.PIE: PieColor.BLACK, Styles.ANSI: 'black'}
|
||||
GREEN = {Styles.PIE: PieColor.GREEN, Styles.ANSI: 'green'}
|
||||
ORANGE = {Styles.PIE: PieColor.ORANGE, Styles.ANSI: 'yellow'}
|
||||
YELLOW = {Styles.PIE: PieColor.YELLOW, Styles.ANSI: 'bright_yellow'}
|
||||
BLUE = {Styles.PIE: PieColor.BLUE, Styles.ANSI: 'blue'}
|
||||
PINK = {Styles.PIE: PieColor.PINK, Styles.ANSI: 'bright_magenta'}
|
||||
PURPLE = {Styles.PIE: PieColor.PURPLE, Styles.ANSI: 'magenta'}
|
||||
RED = {Styles.PIE: PieColor.RED, Styles.ANSI: 'red'}
|
||||
AQUA = {Styles.PIE: PieColor.AQUA, Styles.ANSI: 'cyan'}
|
||||
GREY = {Styles.PIE: PieColor.GREY, Styles.ANSI: 'bright_black'}
|
||||
|
||||
def apply_style(
|
||||
self, style: Styles, *, style_name: Optional[str] = None
|
||||
) -> str:
|
||||
"""Apply the given style to a particular value."""
|
||||
exposed_color = self.value[style]
|
||||
if style is Styles.PIE:
|
||||
assert style_name is not None
|
||||
shade = PIE_STYLE_TO_SHADE[PieStyle(style_name)]
|
||||
return get_color(exposed_color, shade)
|
||||
else:
|
||||
return exposed_color
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StyledGenericColor:
|
||||
color: 'GenericColor'
|
||||
styles: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# noinspection PyDictCreation
|
||||
COLOR_PALETTE = {
|
||||
'transparent': 'transparent',
|
||||
'current': 'currentColor',
|
||||
'white': '#F5F5F0',
|
||||
'black': '#1C1818',
|
||||
'grey': {
|
||||
# Copy the brand palette
|
||||
PieColor.WHITE: '#F5F5F0',
|
||||
PieColor.BLACK: '#1C1818',
|
||||
PieColor.GREY: {
|
||||
'50': '#F5F5F0',
|
||||
'100': '#EDEDEB',
|
||||
'200': '#D1D1CF',
|
||||
@ -19,7 +125,7 @@ COLOR_PALETTE = {
|
||||
'900': '#1C1818',
|
||||
'DEFAULT': '#7D7D7D',
|
||||
},
|
||||
'aqua': {
|
||||
PieColor.AQUA: {
|
||||
'50': '#E8F0F5',
|
||||
'100': '#D6E3ED',
|
||||
'200': '#C4D9E5',
|
||||
@ -32,7 +138,7 @@ COLOR_PALETTE = {
|
||||
'900': '#455966',
|
||||
'DEFAULT': '#8CB4CD',
|
||||
},
|
||||
'purple': {
|
||||
PieColor.PURPLE: {
|
||||
'50': '#F0E0FC',
|
||||
'100': '#E3C7FA',
|
||||
'200': '#D9ADF7',
|
||||
@ -45,7 +151,7 @@ COLOR_PALETTE = {
|
||||
'900': '#5C2982',
|
||||
'DEFAULT': '#B464F0',
|
||||
},
|
||||
'orange': {
|
||||
PieColor.ORANGE: {
|
||||
'50': '#FFEDDB',
|
||||
'100': '#FFDEBF',
|
||||
'200': '#FFCFA3',
|
||||
@ -58,7 +164,7 @@ COLOR_PALETTE = {
|
||||
'900': '#C75E0A',
|
||||
'DEFAULT': '#FFA24E',
|
||||
},
|
||||
'red': {
|
||||
PieColor.RED: {
|
||||
'50': '#FFE0DE',
|
||||
'100': '#FFC7C4',
|
||||
'200': '#FFB0AB',
|
||||
@ -71,7 +177,7 @@ COLOR_PALETTE = {
|
||||
'900': '#910A00',
|
||||
'DEFAULT': '#FF665B',
|
||||
},
|
||||
'blue': {
|
||||
PieColor.BLUE: {
|
||||
'50': '#DBE3FA',
|
||||
'100': '#BFCFF5',
|
||||
'200': '#A1B8F2',
|
||||
@ -84,7 +190,7 @@ COLOR_PALETTE = {
|
||||
'900': '#2B478F',
|
||||
'DEFAULT': '#4B78E6',
|
||||
},
|
||||
'pink': {
|
||||
PieColor.PINK: {
|
||||
'50': '#FFEBFF',
|
||||
'100': '#FCDBFC',
|
||||
'200': '#FCCCFC',
|
||||
@ -97,7 +203,7 @@ COLOR_PALETTE = {
|
||||
'900': '#8C3D8A',
|
||||
'DEFAULT': '#FA9BFA',
|
||||
},
|
||||
'green': {
|
||||
PieColor.GREEN: {
|
||||
'50': '#E3F7E8',
|
||||
'100': '#CCF2D6',
|
||||
'200': '#B5EDC4',
|
||||
@ -110,7 +216,7 @@ COLOR_PALETTE = {
|
||||
'900': '#307842',
|
||||
'DEFAULT': '#73DC8C',
|
||||
},
|
||||
'yellow': {
|
||||
PieColor.YELLOW: {
|
||||
'50': '#F7F7DB',
|
||||
'100': '#F2F2BF',
|
||||
'200': '#EDEDA6',
|
||||
@ -124,37 +230,39 @@ COLOR_PALETTE = {
|
||||
'DEFAULT': '#DBDE52',
|
||||
},
|
||||
}
|
||||
|
||||
# Grey is the same no matter shade for the colors
|
||||
COLOR_PALETTE['grey'] = {
|
||||
shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys()
|
||||
}
|
||||
|
||||
COLOR_PALETTE['primary'] = {
|
||||
'700': COLOR_PALETTE['black'],
|
||||
'600': 'ansibrightblack',
|
||||
'500': COLOR_PALETTE['white'],
|
||||
}
|
||||
|
||||
COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'}
|
||||
|
||||
SHADE_NAMES = {
|
||||
'500': 'pie-dark',
|
||||
'600': 'pie',
|
||||
'700': 'pie-light'
|
||||
}
|
||||
|
||||
SHADES = [
|
||||
'50',
|
||||
*map(str, range(100, 1000, 100))
|
||||
]
|
||||
COLOR_PALETTE.update(
|
||||
{
|
||||
# Terminal-specific palette customizations.
|
||||
PieColor.GREY: {
|
||||
# Grey is the same no matter shade for the colors
|
||||
shade: COLOR_PALETTE[PieColor.GREY]['500']
|
||||
for shade in COLOR_PALETTE[PieColor.GREY].keys()
|
||||
},
|
||||
PieColor.PRIMARY: {
|
||||
'700': COLOR_PALETTE[PieColor.BLACK],
|
||||
'600': PYGMENTS_BRIGHT_BLACK,
|
||||
'500': COLOR_PALETTE[PieColor.WHITE],
|
||||
},
|
||||
PieColor.SECONDARY: {
|
||||
'700': '#37523C',
|
||||
'600': '#6c6969',
|
||||
'500': '#6c6969',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_color(color: str, shade: str) -> Optional[str]:
|
||||
if color not in COLOR_PALETTE:
|
||||
def boldify(color: PieColor) -> str:
|
||||
return f'bold {color}'
|
||||
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def get_color(
|
||||
color: PieColor, shade: str, *, palette=COLOR_PALETTE
|
||||
) -> Optional[str]:
|
||||
if color not in palette:
|
||||
return None
|
||||
|
||||
color_code = COLOR_PALETTE[color]
|
||||
color_code = palette[color]
|
||||
if isinstance(color_code, dict) and shade in color_code:
|
||||
return color_code[shade]
|
||||
else:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user