mirror of
https://github.com/httpie/cli.git
synced 2025-08-10 09:37:50 +02:00
Compare commits
184 Commits
Author | SHA1 | Date | |
---|---|---|---|
88140422a9 | |||
d2d40eb336 | |||
cd877a5e08 | |||
87629706c9 | |||
3856f94d3d | |||
dc30919893 | |||
fb82f44cd1 | |||
eb4e32ca28 | |||
980bd59e29 | |||
2cda966384 | |||
7bf373751d | |||
21faddc4b9 | |||
c126bc11c7 | |||
00c859c51d | |||
508788ca56 | |||
4c56d894ba | |||
0e10e23dca | |||
06512c72a3 | |||
8d84248ee3 | |||
17ed3bb8c5 | |||
05c02f0f39 | |||
0ebc9a7e09 | |||
c692669526 | |||
747accc2ae | |||
f3b500119c | |||
e0e03f3237 | |||
be87da8bbd | |||
e09401b81a | |||
5a83a9ebc4 | |||
c97ec93a19 | |||
2d15659b16 | |||
021b41c9e5 | |||
8dc6c0df77 | |||
1bd8422fb5 | |||
c237e15108 | |||
a5d8b51e47 | |||
2b78d04410 | |||
7bd7aa20d2 | |||
7ae44aefe2 | |||
28e874535a | |||
340fef6278 | |||
088b6cdb0c | |||
43462f8af0 | |||
e4b2751a52 | |||
f94c12d8ca | |||
3db1cdba4c | |||
4f7f59b990 | |||
e30ec6be42 | |||
207b970d94 | |||
62e43abc86 | |||
ea8e22677a | |||
df58ec683e | |||
8fe1f08a37 | |||
521ddde4c5 | |||
3457806df1 | |||
840f77d2a8 | |||
6522ce06d0 | |||
f927065416 | |||
151becec2b | |||
ba8e4097e8 | |||
00b366a81f | |||
5bf696d113 | |||
3081fc1a3c | |||
245cede2c2 | |||
6bdcdf1eba | |||
0fc6331ee0 | |||
ef62fc11bf | |||
c000886546 | |||
cfcd7413d1 | |||
7dfa001d2c | |||
06d9c14e7a | |||
861b8b36a8 | |||
434512e92f | |||
72735d9d59 | |||
7cdd74fece | |||
d40f06687f | |||
0d9c8b88b3 | |||
cff45276b5 | |||
e75e0a0565 | |||
19e48ba901 | |||
a9b8513f62 | |||
7985cf60c8 | |||
5dc4a26277 | |||
7775422afb | |||
2be43e698a | |||
3abc76f6d5 | |||
021eb651e0 | |||
419427cfb6 | |||
7500912be1 | |||
1b4048aefc | |||
7885f5cd66 | |||
3e414d731c | |||
d8f6a5fe52 | |||
cee283a01a | |||
5c267003c7 | |||
cdab8e67cb | |||
6c6093a46d | |||
42af2f786f | |||
a65771e271 | |||
7b683d4b57 | |||
a15fd6f966 | |||
19691bba68 | |||
344491ba8e | |||
9f6fa090df | |||
59f4ef03cc | |||
ef92e2a74a | |||
1171984ec2 | |||
ce9746b1f8 | |||
6b99e1c932 | |||
7d418aecd0 | |||
459cdfcf53 | |||
ab8512f96c | |||
6befaf9067 | |||
1b7f74c2b2 | |||
50f57f8c82 | |||
555afef486 | |||
476eb4f0d9 | |||
3869c3ce99 | |||
17f74f10f3 | |||
1e094d0a79 | |||
bd227c0364 | |||
9dda23a322 | |||
ef4fa20ceb | |||
7e0bed4e54 | |||
e1627803fe | |||
f954c9e2b7 | |||
80e83f0463 | |||
4f1c9441c5 | |||
7989e438d2 | |||
93114072c8 | |||
08751d3672 | |||
0c9d701618 | |||
a3fa016428 | |||
9c52449344 | |||
e4e4927567 | |||
031b4b89e3 | |||
e1c08a3de5 | |||
033798adc1 | |||
6a4e985f71 | |||
a6c70334cf | |||
7388401134 | |||
4ef31ecf71 | |||
2423f893e5 | |||
b6a694afbc | |||
71adcd97d0 | |||
b50f9aa7e7 | |||
fe96b2af20 | |||
727b8a2c05 | |||
9c89c703ae | |||
8f8851f1db | |||
bce2b3a98e | |||
474093acdf | |||
1535d0c976 | |||
cae83b3f9e | |||
507514b795 | |||
d7ed45bbcd | |||
e6c5cd3e4b | |||
273134123a | |||
529aa78ee1 | |||
e2ba214ac0 | |||
9dd0203bae | |||
ba6fd0bc14 | |||
8f7f4a6ef4 | |||
9984447f18 | |||
10081b9fcc | |||
4f84362d73 | |||
2b5f8f48bf | |||
a51068a44d | |||
f06d870012 | |||
0115a4a466 | |||
7c1d26a8fa | |||
7734e47280 | |||
30c595b770 | |||
b38352858f | |||
a45b94fda6 | |||
513e5080e4 | |||
7c9f415107 | |||
4c8633c6e5 | |||
4d7d6b66cf | |||
a586fca246 | |||
978258ec5b | |||
84ef9f588c | |||
cf21790411 | |||
1ef127c61d |
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
18
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,34 +7,38 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Checklist**
|
||||
## Checklist
|
||||
|
||||
- [ ] I've searched for similar issues.
|
||||
- [ ] I'm using the latest version of HTTPie.
|
||||
|
||||
---
|
||||
|
||||
**What are the steps to reproduce the problem?**
|
||||
## Minimal reproduction code and steps
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Current result
|
||||
|
||||
**What is the expected result?**
|
||||
…
|
||||
|
||||
## Expected result
|
||||
|
||||
**What happens instead?**
|
||||
…
|
||||
|
||||
---
|
||||
|
||||
**Debug output**
|
||||
## Debug output
|
||||
|
||||
Please re-run the command with `--debug`, then copy the entire command & output and paste both below:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
|
||||
<COMPLETE OUTPUT>
|
||||
```
|
||||
|
||||
## Additional information, screenshots, or code examples
|
||||
|
||||
**Provide any additional information, screenshots, or code examples below:**
|
||||
…
|
||||
|
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -6,19 +6,25 @@ labels: "new, enhancement"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
**Checklist**
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I've searched for similar feature requests.
|
||||
|
||||
---
|
||||
|
||||
**What enhancement would you like to see?**
|
||||
## Enhancement request
|
||||
|
||||
…
|
||||
|
||||
**What problem does it solve?**
|
||||
---
|
||||
|
||||
E.g. “I'm always frustrated when [...]”, “I’m trying to do […] so that […]”.
|
||||
## Problem it solves
|
||||
|
||||
E.g. “I'm always frustrated when […]”, “I’m trying to do […] so that […]”.
|
||||
|
||||
**Provide any additional information, screenshots, or code examples below:**
|
||||
---
|
||||
|
||||
## Additional information, screenshots, or code examples
|
||||
|
||||
…
|
||||
|
52
.github/workflows/benchmark.yml
vendored
Normal file
52
.github/workflows/benchmark.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.event.label.name == 'benchmark'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- id: benchmarks
|
||||
name: Run Benchmarks
|
||||
run: |
|
||||
python -m pip install pyperf>=2.3.0
|
||||
python extras/profiling/run.py --fresh --complex --min-speed=6 --file output.txt
|
||||
body=$(cat output.txt)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo "::set-output name=body::$body"
|
||||
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '# Benchmarks'
|
||||
|
||||
- name: Create or update comment
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
# Benchmarks
|
||||
${{ steps.benchmarks.outputs.body }}
|
||||
edit-mode: replace
|
||||
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: benchmark
|
35
.github/workflows/build.yml
vendored
35
.github/workflows/build.yml
vendored
@ -1,35 +0,0 @@
|
||||
name: Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
extras:
|
||||
# Run coverage and extra tests only once
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: python -m pip install --upgrade pip setuptools wheel
|
||||
- run: make install
|
||||
- run: make codestyle
|
||||
- run: make test-cover
|
||||
- run: make codecov-upload
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_REPO_TOKEN }}
|
||||
- run: make test-dist
|
||||
test:
|
||||
# Run core HTTPie tests everywhere
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, "3.10.0-rc.1"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- run: python -m pip install --upgrade pip setuptools wheel
|
||||
- run: python -m pip install --upgrade '.[dev]'
|
||||
- run: python -m pytest --verbose ./httpie ./tests
|
19
.github/workflows/code-style.yml
vendored
Normal file
19
.github/workflows/code-style.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/code-style.yml
|
||||
- extras/*.py
|
||||
- httpie/**/*.py
|
||||
- setup.py
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make venv
|
||||
- run: make codestyle
|
22
.github/workflows/coverage.yml
vendored
Normal file
22
.github/workflows/coverage.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/coverage.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- run: make install
|
||||
- run: make test-cover
|
||||
- run: make codecov-upload
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_REPO_TOKEN }}
|
||||
- run: make test-dist
|
19
.github/workflows/docs-check-markdown.yml
vendored
Normal file
19
.github/workflows/docs-check-markdown.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "*.md"
|
||||
- "**/*.md"
|
||||
|
||||
jobs:
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- name: Install the linter
|
||||
run: sudo gem install mdl
|
||||
- name: Check files
|
||||
run: make doc-check
|
20
.github/workflows/docs-deploy.yml
vendored
Normal file
20
.github/workflows/docs-deploy.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- docs/README.md
|
||||
- docs/config.json
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- unpublished
|
||||
- deleted
|
||||
jobs:
|
||||
trigger-doc-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install HTTPie
|
||||
run: sudo snap install --edge httpie
|
||||
- name: Trigger new documentation build
|
||||
run: http --ignore-stdin POST ${{ secrets.DOCS_UPDATE_VERCEL_HOOK }}
|
31
.github/workflows/docs-update-install.yml
vendored
Normal file
31
.github/workflows/docs-update-install.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
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 }}
|
22
.github/workflows/release-snap.yml
vendored
Normal file
22
.github/workflows/release-snap.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
|
||||
jobs:
|
||||
snap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
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
|
31
.github/workflows/release.yml
vendored
Normal file
31
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
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 }}
|
26
.github/workflows/stale.yml
vendored
Normal file
26
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Mark stale pull requests
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
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'
|
||||
|
||||
days-before-stale: -1
|
||||
days-before-issue-stale: -1
|
||||
days-before-pr-stale: 30
|
||||
|
||||
days-before-close: -1
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-close: 0
|
||||
|
||||
operations-per-run: 300
|
25
.github/workflows/test-package-linux-snap.yml
vendored
Normal file
25
.github/workflows/test-package-linux-snap.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/test-package-linux-snap.yml
|
||||
- snapcraft.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
snap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
uses: snapcore/action-build@v1
|
||||
id: snapcraft
|
||||
- name: Install
|
||||
run: sudo snap install --dangerous ${{ steps.snapcraft.outputs.snap }}
|
||||
- name: Test
|
||||
run: |
|
||||
httpie.http --version
|
||||
httpie.https --version
|
||||
httpie --version
|
||||
# Auto-aliases cannot be tested when installing a snap outside the store.
|
||||
# http --version
|
||||
# https --version
|
18
.github/workflows/test-package-mac-brew.yml
vendored
Normal file
18
.github/workflows/test-package-mac-brew.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/test-package-mac-brew.yml
|
||||
- docs/packaging/brew/httpie.rb
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
brew:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup brew
|
||||
run: |
|
||||
brew developer on
|
||||
brew update
|
||||
- name: Build and test the formula
|
||||
run: make brew-test
|
45
.github/workflows/tests.yml
vendored
Normal file
45
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/tests.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/tests.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [3.7, 3.8, 3.9, "3.10"]
|
||||
pyopenssl: [0, 1]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Windows setup
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install --upgrade '.[dev]'
|
||||
python -m pytest --verbose ./httpie ./tests
|
||||
env:
|
||||
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
|
||||
- name: Linux & Mac setup
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
make install
|
||||
make test
|
||||
env:
|
||||
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -118,6 +118,7 @@ celerybeat.pid
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
venv*/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
@ -144,3 +145,9 @@ dmypy.json
|
||||
/httpie.spec
|
||||
/httpie-*.rpm
|
||||
/httpie-*.tar.gz
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# Windows Chocolatey
|
||||
*.nupkg
|
||||
|
@ -2,10 +2,9 @@
|
||||
# https://packit.dev/docs/configuration/
|
||||
specfile_path: httpie.spec
|
||||
actions:
|
||||
# the current Fedora Rawhide specfile has some patches
|
||||
# so we get it from @hroncok's (= churchyard in Fedora) fork for now
|
||||
# once we have a new release, we'll use: https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec
|
||||
post-upstream-clone: "wget https://src.fedoraproject.org/fork/churchyard/rpms/httpie/raw/packit/f/httpie.spec -O httpie.spec"
|
||||
# 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"
|
||||
jobs:
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -3,8 +3,45 @@
|
||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [3.0.0](https://github.com/httpie/httpie/compare/2.6.0...3.0.0) (2022-01-21)
|
||||
|
||||
- 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))
|
||||
- Added `httpie plugins` interface for plugin management. ([#566](https://github.com/httpie/httpie/issues/566))
|
||||
- Added support for Bearer authentication via `--auth-type=bearer` ([#1215](https://github.com/httpie/httpie/issues/1215)).
|
||||
- Added support for quick conversions of pasted URLs into HTTPie calls by adding a space after the protocol name (`$ https ://pie.dev` → `https://pie.dev`). ([#1195](https://github.com/httpie/httpie/issues/1195))
|
||||
- Added support for _sending_ multiple HTTP header lines with the same name. ([#130](https://github.com/httpie/httpie/issues/130))
|
||||
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
|
||||
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
|
||||
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
|
||||
- Added support for displaying the total elapsed time through `--meta`/`-vv` or `--print=m`. ([#243](https://github.com/httpie/httpie/issues/243))
|
||||
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
|
||||
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
|
||||
- Added support for storing prompted passwords in the local sessions. ([#1098](https://github.com/httpie/httpie/issues/1098))
|
||||
- Added warnings about the `--ignore-stdin`, when there is no incoming data from stdin. ([#1255](https://github.com/httpie/httpie/issues/1255))
|
||||
- Fixed crashing due to broken plugins. ([#1204](https://github.com/httpie/httpie/issues/1204))
|
||||
- Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156))
|
||||
- Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242))
|
||||
- Fixed an unexpected crash when `--raw` is used with `--chunked`. ([#1253](https://github.com/httpie/httpie/issues/1253))
|
||||
- Changed the default Windows theme from `fruity` to `auto`. ([#1266](https://github.com/httpie/httpie/issues/1266))
|
||||
|
||||
## [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)
|
||||
|
||||
- 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))
|
||||
- Added `--response-charset` to allow overriding the response encoding for terminal display purposes. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
- Added `--response-mime` to allow overriding the response mime type for coloring and formatting for the terminal. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
- Added the ability to silence warnings through using `-q` or `--quiet` twice (e.g. `-qq`) ([#1175](https://github.com/httpie/httpie/issues/1175))
|
||||
- Added installed plugin list to `--debug` output. ([#1165](https://github.com/httpie/httpie/issues/1165))
|
||||
- Fixed duplicate keys preservation in JSON data. ([#1163](https://github.com/httpie/httpie/issues/1163))
|
||||
|
||||
## [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)
|
||||
|
||||
- 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))
|
||||
- Added support for XML formatting. ([#1129](https://github.com/httpie/httpie/issues/1129))
|
||||
|
@ -68,7 +68,7 @@ members of the project's leadership.
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||
version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
<https://www.contributor-covenant.org/faq>
|
||||
|
@ -44,7 +44,7 @@ Consider also adding a [CHANGELOG](https://github.com/httpie/httpie/blob/master/
|
||||
|
||||
#### Getting the code
|
||||
|
||||
Go to https://github.com/httpie/httpie and fork the project repository.
|
||||
Go to <https://github.com/httpie/httpie> and fork the project repository.
|
||||
|
||||
```bash
|
||||
# Clone your fork
|
||||
@ -89,7 +89,7 @@ a hack but it works™.)
|
||||
You should now see `(httpie)` next to your shell prompt, and
|
||||
the `http` command should point to your development copy:
|
||||
|
||||
```
|
||||
```bash
|
||||
(httpie) ~/Code/httpie $ which http
|
||||
/Users/<user>/Code/httpie/venv/bin/http
|
||||
(httpie) ~/Code/httpie $ http --version
|
||||
@ -144,11 +144,25 @@ $ python -m pytest tests/test_uploads.py::TestMultipartFormDataFileUpload::test_
|
||||
|
||||
See [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) for additional development utilities.
|
||||
|
||||
#### Running benchmarks
|
||||
|
||||
If you are trying to work on speeding up HTTPie and want to verify your results, you
|
||||
can run the benchmark suite. The suite will compare the last commit of your branch
|
||||
with the master branch of your repository (or a fresh checkout of HTTPie master, through
|
||||
`--fresh`) and report the results back.
|
||||
|
||||
```bash
|
||||
$ python extras/benchmarks/run.py
|
||||
```
|
||||
|
||||
The benchmarks can also be run on the CI. Since it is a long process, it requires manual
|
||||
oversight. Ping one of the maintainers to get a `benchmark` label on your branch.
|
||||
|
||||
#### Windows
|
||||
|
||||
If you are on a Windows machine and not able to run `make`,
|
||||
follow the next steps for a basic setup. As a prerequisite, you need to have
|
||||
Python 3.6+ installed.
|
||||
Python 3.7+ installed.
|
||||
|
||||
Create a virtual environment and activate it:
|
||||
|
||||
@ -160,7 +174,7 @@ C:\> venv\Scripts\activate
|
||||
Install HTTPie in editable mode with all the dependencies:
|
||||
|
||||
```powershell
|
||||
C:\> python -m pip install --upgrade -e . -r requirements-dev.txt
|
||||
C:\> python -m pip install --upgrade -e .[dev]
|
||||
```
|
||||
|
||||
You should now see `(httpie)` next to your shell prompt, and
|
||||
@ -168,19 +182,19 @@ the `http` command should point to your development copy:
|
||||
|
||||
```powershell
|
||||
# In PowerShell:
|
||||
(httpie) PS C:\Users\ovezovs\httpie> Get-Command http
|
||||
(httpie) PS C:\Users\<user>\httpie> Get-Command http
|
||||
CommandType Name Version Source
|
||||
----------- ---- ------- ------
|
||||
Application http.exe 0.0.0.0 C:\Users\ovezovs\httpie\venv\Scripts\http.exe
|
||||
Application http.exe 0.0.0.0 C:\Users\<user>\httpie\venv\Scripts\http.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
# In CMD:
|
||||
(httpie) C:\Users\ovezovs\httpie> where http
|
||||
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
|
||||
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
|
||||
(httpie) C:\Users\<user>\httpie> where http
|
||||
C:\Users\<user>\httpie\venv\Scripts\http.exe
|
||||
C:\Users\<user>\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
|
||||
|
||||
(httpie) C:\Users\ovezovs\httpie> http --version
|
||||
(httpie) C:\Users\<user>\httpie> http --version
|
||||
2.3.0-dev
|
||||
```
|
||||
|
||||
|
36
Makefile
36
Makefile
@ -25,7 +25,13 @@ export PATH := $(VENV_BIN):$(PATH)
|
||||
all: uninstall-httpie install test
|
||||
|
||||
|
||||
install: venv
|
||||
install: venv install-reqs
|
||||
|
||||
|
||||
install-reqs:
|
||||
@echo $(H1)Updating package tools$(H1END)
|
||||
$(VENV_PIP) install --upgrade pip wheel
|
||||
|
||||
@echo $(H1)Installing dev requirements$(H1END)
|
||||
$(VENV_PIP) install --upgrade --editable '.[dev]'
|
||||
|
||||
@ -34,6 +40,7 @@ install: venv
|
||||
|
||||
@echo
|
||||
|
||||
|
||||
clean:
|
||||
@echo $(H1)Cleaning up$(H1END)
|
||||
rm -rf $(VENV_ROOT)
|
||||
@ -77,11 +84,11 @@ venv:
|
||||
|
||||
test:
|
||||
@echo $(H1)Running tests$(HEADER_EXTRA)$(H1END)
|
||||
$(VENV_BIN)/python -m pytest $(COV) ./httpie $(COV) ./tests --doctest-modules --verbose ./httpie ./tests
|
||||
$(VENV_BIN)/python -m pytest $(COV)
|
||||
@echo
|
||||
|
||||
|
||||
test-cover: COV=--cov
|
||||
test-cover: COV=--cov=httpie --cov=tests
|
||||
test-cover: HEADER_EXTRA=' (with coverage)'
|
||||
test-cover: test
|
||||
|
||||
@ -123,7 +130,7 @@ pycodestyle: codestyle
|
||||
codestyle:
|
||||
@echo $(H1)Running flake8$(H1END)
|
||||
@[ -f $(VENV_BIN)/flake8 ] || $(VENV_PIP) install --upgrade --editable '.[dev]'
|
||||
$(VENV_BIN)/flake8 httpie/ tests/ extras/ *.py
|
||||
$(VENV_BIN)/flake8 httpie/ tests/ extras/profiling/ docs/packaging/brew/ *.py
|
||||
@echo
|
||||
|
||||
|
||||
@ -135,6 +142,16 @@ codecov-upload:
|
||||
@echo
|
||||
|
||||
|
||||
doc-check:
|
||||
@echo $(H1)Running documentations checks$(H1END)
|
||||
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
|
||||
###############################################################################
|
||||
@ -179,10 +196,17 @@ uninstall-httpie:
|
||||
###############################################################################
|
||||
|
||||
brew-deps:
|
||||
extras/brew-deps.py
|
||||
docs/packaging/brew/brew-deps.py
|
||||
|
||||
brew-test:
|
||||
@echo $(H1)Uninstalling httpie$(H1END)
|
||||
- brew uninstall httpie
|
||||
brew install --build-from-source ./extras/httpie.rb
|
||||
|
||||
@echo $(H1)Building from source…$(H1END)
|
||||
- brew install --build-from-source ./docs/packaging/brew/httpie.rb
|
||||
|
||||
@echo $(H1)Verifying…$(H1END)
|
||||
brew test httpie
|
||||
|
||||
@echo $(H1)Auditing…$(H1END)
|
||||
brew audit --strict httpie
|
||||
|
14
README.md
14
README.md
@ -17,7 +17,7 @@ They use simple and natural syntax and provide formatted and colorized output.
|
||||
[](https://github.com/httpie/httpie/actions)
|
||||
[](https://codecov.io/gh/httpie/httpie)
|
||||
[](https://twitter.com/httpie)
|
||||
[](https://httpie.io/chat)
|
||||
[](https://httpie.io/discord)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
|
||||
|
||||
@ -38,31 +38,31 @@ They use simple and natural syntax and provide formatted and colorized output.
|
||||
- Persistent sessions
|
||||
- `wget`-like downloads
|
||||
|
||||
[See for all features →](https://httpie.io/docs)
|
||||
[See all features →](https://httpie.io/docs)
|
||||
|
||||
## Examples
|
||||
|
||||
Hello World:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ https httpie.io/hello
|
||||
```
|
||||
|
||||
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ http PUT pie.dev/put X-API-Token:123 name=John
|
||||
```
|
||||
|
||||
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
|
||||
|
||||
```
|
||||
```bash
|
||||
$ http --offline pie.dev/post hello=offline
|
||||
```
|
||||
|
||||
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
|
||||
|
||||
```
|
||||
```bash
|
||||
$ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
|
||||
```
|
||||
|
||||
@ -71,7 +71,7 @@ $ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/com
|
||||
## Community & support
|
||||
|
||||
- Visit the [HTTPie website](https://httpie.io) for full documentation and useful links.
|
||||
- Join our [Discord server](https://httpie.io/chat) is to ask questions, discuss features, and for general API chat.
|
||||
- Join our [Discord server](https://httpie.io/discord) is to ask questions, discuss features, and for general API chat.
|
||||
- Tweet at [@httpie](https://twitter.com/httpie) on Twitter.
|
||||
- Use [StackOverflow](https://stackoverflow.com/questions/tagged/httpie) to ask questions and include a `httpie` tag.
|
||||
- Create [GitHub Issues](https://github.com/httpie/httpie/issues) for bug reports and feature requests.
|
||||
|
973
docs/README.md
973
docs/README.md
File diff suppressed because it is too large
Load Diff
5
docs/config.json
Normal file
5
docs/config.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"website": {
|
||||
"master_and_released_docs_differ_after": "d40f06687f8cbbd22bf7dba05bee93aea11a169f"
|
||||
}
|
||||
}
|
3
docs/contributors/README.md
Normal file
3
docs/contributors/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
Here we maintain a database of contributors, from which we generate credits on release blog posts and social medias.
|
||||
|
||||
For the HTTPie blog see: <https://httpie.io/blog>.
|
280
docs/contributors/fetch.py
Normal file
280
docs/contributors/fetch.py
Normal file
@ -0,0 +1,280 @@
|
||||
"""
|
||||
Generate the contributors database.
|
||||
|
||||
FIXME: replace `requests` calls with the HTTPie API, when available.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
import requests
|
||||
|
||||
FullNames = Set[str]
|
||||
GitHubLogins = Set[str]
|
||||
Person = Dict[str, str]
|
||||
People = Dict[str, Person]
|
||||
UserInfo = Dict[str, Any]
|
||||
|
||||
CO_AUTHORS = re.compile(r'Co-authored-by: ([^<]+) <').finditer
|
||||
API_URL = 'https://api.github.com'
|
||||
REPO = OWNER = 'httpie'
|
||||
REPO_URL = f'{API_URL}/repos/{REPO}/{OWNER}'
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
DB_FILE = HERE / 'people.json'
|
||||
|
||||
DEFAULT_PERSON: Person = {'committed': [], 'reported': [], 'github': '', 'twitter': ''}
|
||||
SKIPPED_LABELS = {'invalid'}
|
||||
|
||||
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
|
||||
assert GITHUB_TOKEN, 'GITHUB_TOKEN envar is missing'
|
||||
|
||||
|
||||
class FinishedForNow(Exception):
|
||||
"""Raised when remaining GitHub rate limit is zero."""
|
||||
|
||||
|
||||
def main(previous_release: str, current_release: str) -> int:
|
||||
since = release_date(previous_release)
|
||||
until = release_date(current_release)
|
||||
|
||||
contributors = load_awesome_people()
|
||||
try:
|
||||
committers = find_committers(since, until)
|
||||
reporters = find_reporters(since, until)
|
||||
except Exception as exc:
|
||||
# We want to save what we fetched so far. So pass.
|
||||
print(' !! ', exc)
|
||||
|
||||
try:
|
||||
merge_all_the_people(current_release, contributors, committers, reporters)
|
||||
fetch_missing_users_details(contributors)
|
||||
except FinishedForNow:
|
||||
# We want to save what we fetched so far. So pass.
|
||||
print(' !! Committers:', committers)
|
||||
print(' !! Reporters:', reporters)
|
||||
exit_status = 1
|
||||
else:
|
||||
exit_status = 0
|
||||
|
||||
save_awesome_people(contributors)
|
||||
return exit_status
|
||||
|
||||
|
||||
def find_committers(since: str, until: str) -> FullNames:
|
||||
url = f'{REPO_URL}/commits'
|
||||
page = 1
|
||||
per_page = 100
|
||||
params = {
|
||||
'since': since,
|
||||
'until': until,
|
||||
'per_page': per_page,
|
||||
}
|
||||
committers: FullNames = set()
|
||||
|
||||
while 'there are commits':
|
||||
params['page'] = page
|
||||
data = fetch(url, params=params)
|
||||
|
||||
for item in data:
|
||||
commit = item['commit']
|
||||
committers.add(commit['author']['name'])
|
||||
debug(' >>> Commit', item['html_url'])
|
||||
for co_author in CO_AUTHORS(commit['message']):
|
||||
name = co_author.group(1)
|
||||
committers.add(name)
|
||||
|
||||
if len(data) < per_page:
|
||||
break
|
||||
page += 1
|
||||
|
||||
return committers
|
||||
|
||||
|
||||
def find_reporters(since: str, until: str) -> GitHubLogins:
|
||||
url = f'{API_URL}/search/issues'
|
||||
page = 1
|
||||
per_page = 100
|
||||
params = {
|
||||
'q': f'repo:{REPO}/{OWNER} is:issue closed:{since}..{until}',
|
||||
'per_page': per_page,
|
||||
}
|
||||
reporters: GitHubLogins = set()
|
||||
|
||||
while 'there are issues':
|
||||
params['page'] = page
|
||||
data = fetch(url, params=params)
|
||||
|
||||
for item in data['items']:
|
||||
# Filter out unwanted labels.
|
||||
if any(label['name'] in SKIPPED_LABELS for label in item['labels']):
|
||||
continue
|
||||
debug(' >>> Issue', item['html_url'])
|
||||
reporters.add(item['user']['login'])
|
||||
|
||||
if len(data['items']) < per_page:
|
||||
break
|
||||
page += 1
|
||||
|
||||
return reporters
|
||||
|
||||
|
||||
def merge_all_the_people(release: str, contributors: People, committers: FullNames, reporters: GitHubLogins) -> None:
|
||||
"""
|
||||
>>> contributors = {'Alice': new_person(github='alice', twitter='alice')}
|
||||
>>> merge_all_the_people('2.6.0', contributors, {}, {})
|
||||
>>> contributors
|
||||
{'Alice': {'committed': [], 'reported': [], 'github': 'alice', 'twitter': 'alice'}}
|
||||
|
||||
>>> contributors = {'Bob': new_person(github='bob', twitter='bob')}
|
||||
>>> merge_all_the_people('2.6.0', contributors, {'Bob'}, {'bob'})
|
||||
>>> contributors
|
||||
{'Bob': {'committed': ['2.6.0'], 'reported': ['2.6.0'], 'github': 'bob', 'twitter': 'bob'}}
|
||||
|
||||
>>> contributors = {'Charlotte': new_person(github='charlotte', twitter='charlotte', committed=['2.5.0'], reported=['2.5.0'])}
|
||||
>>> merge_all_the_people('2.6.0', contributors, {'Charlotte'}, {'charlotte'})
|
||||
>>> contributors
|
||||
{'Charlotte': {'committed': ['2.5.0', '2.6.0'], 'reported': ['2.5.0', '2.6.0'], 'github': 'charlotte', 'twitter': 'charlotte'}}
|
||||
|
||||
"""
|
||||
# Update known contributors.
|
||||
for name, details in contributors.items():
|
||||
if name in committers:
|
||||
if release not in details['committed']:
|
||||
details['committed'].append(release)
|
||||
committers.remove(name)
|
||||
if details['github'] in reporters:
|
||||
if release not in details['reported']:
|
||||
details['reported'].append(release)
|
||||
reporters.remove(details['github'])
|
||||
|
||||
# Add new committers.
|
||||
for name in committers:
|
||||
user_info = user(fullname=name)
|
||||
contributors[name] = new_person(
|
||||
github=user_info['login'],
|
||||
twitter=user_info['twitter_username'],
|
||||
committed=[release],
|
||||
)
|
||||
if user_info['login'] in reporters:
|
||||
contributors[name]['reported'].append(release)
|
||||
reporters.remove(user_info['login'])
|
||||
|
||||
# Add new reporters.
|
||||
for github_username in reporters:
|
||||
user_info = user(github_username=github_username)
|
||||
contributors[user_info['name'] or user_info['login']] = new_person(
|
||||
github=github_username,
|
||||
twitter=user_info['twitter_username'],
|
||||
reported=[release],
|
||||
)
|
||||
|
||||
|
||||
def release_date(release: str) -> str:
|
||||
date = check_output(['git', 'log', '-1', '--format=%ai', release], text=True).strip()
|
||||
return datetime.strptime(date, '%Y-%m-%d %H:%M:%S %z').isoformat()
|
||||
|
||||
|
||||
def load_awesome_people() -> People:
|
||||
try:
|
||||
with DB_FILE.open(encoding='utf-8') as fh:
|
||||
return json.load(fh)
|
||||
except (FileNotFoundError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def fetch(url: str, params: Optional[Dict[str, str]] = None) -> UserInfo:
|
||||
headers = {
|
||||
'Accept': 'application/vnd.github.v3+json',
|
||||
'Authentication': f'token {GITHUB_TOKEN}'
|
||||
}
|
||||
for retry in range(1, 6):
|
||||
debug(f'[{retry}/5]', f'{url = }', f'{params = }')
|
||||
with requests.get(url, params=params, headers=headers) as req:
|
||||
try:
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.HTTPError as exc:
|
||||
if exc.response.status_code == 403:
|
||||
# 403 Client Error: rate limit exceeded for url: ...
|
||||
now = int(datetime.utcnow().timestamp())
|
||||
xrate_limit_reset = int(exc.response.headers['X-RateLimit-Reset'])
|
||||
wait = xrate_limit_reset - now
|
||||
if wait > 20:
|
||||
raise FinishedForNow()
|
||||
debug(' !', 'Waiting', wait, 'seconds before another try ...')
|
||||
sleep(wait)
|
||||
continue
|
||||
return req.json()
|
||||
assert ValueError('Rate limit exceeded')
|
||||
|
||||
|
||||
def new_person(**kwargs: str) -> Person:
|
||||
data = deepcopy(DEFAULT_PERSON)
|
||||
data.update(**kwargs)
|
||||
return data
|
||||
|
||||
|
||||
def user(fullname: Optional[str] = '', github_username: Optional[str] = '') -> UserInfo:
|
||||
if github_username:
|
||||
url = f'{API_URL}/users/{github_username}'
|
||||
return fetch(url)
|
||||
|
||||
url = f'{API_URL}/search/users'
|
||||
for query in (f'fullname:{fullname}', f'user:{fullname}'):
|
||||
params = {
|
||||
'q': f'repo:{REPO}/{OWNER} {query}',
|
||||
'per_page': 1,
|
||||
}
|
||||
user_info = fetch(url, params=params)
|
||||
if user_info['items']:
|
||||
user_url = user_info['items'][0]['url']
|
||||
return fetch(user_url)
|
||||
|
||||
|
||||
def fetch_missing_users_details(people: People) -> None:
|
||||
for name, details in people.items():
|
||||
if details['github'] and details['twitter']:
|
||||
continue
|
||||
user_info = user(github_username=details['github'], fullname=name)
|
||||
if not details['github']:
|
||||
details['github'] = user_info['login']
|
||||
if not details['twitter']:
|
||||
details['twitter'] = user_info['twitter_username']
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def debug(*args: Any) -> None:
|
||||
if os.getenv('DEBUG') == '1':
|
||||
print(*args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ret = 1
|
||||
try:
|
||||
ret = main(*sys.argv[1:])
|
||||
except TypeError:
|
||||
ret = 2
|
||||
print(f'''
|
||||
Fetch contributors to a release.
|
||||
|
||||
Usage:
|
||||
python {sys.argv[0]} {sys.argv[0]} <RELEASE N-1> <RELEASE N>
|
||||
Example:
|
||||
python {sys.argv[0]} 2.4.0 2.5.0
|
||||
|
||||
Define the DEBUG=1 environment variable to enable verbose output.
|
||||
''')
|
||||
except KeyboardInterrupt:
|
||||
ret = 255
|
||||
sys.exit(ret)
|
45
docs/contributors/generate.py
Normal file
45
docs/contributors/generate.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""
|
||||
Generate snippets to copy-paste.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from fetch import HERE, load_awesome_people
|
||||
|
||||
TPL_FILE = HERE / 'snippet.jinja2'
|
||||
HTTPIE_TEAM = {
|
||||
'claudiatd',
|
||||
'jakubroztocil',
|
||||
'jkbr',
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
and (release in details['committed'] or release in details['reported'])
|
||||
}
|
||||
|
||||
template = Template(source=TPL_FILE.read_text(encoding='utf-8'))
|
||||
output = template.render(contributors=contributors, release=release)
|
||||
print(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ret = 1
|
||||
try:
|
||||
ret = generate_snippets(sys.argv[1])
|
||||
except (IndexError, TypeError):
|
||||
ret = 2
|
||||
print(f'''
|
||||
Generate snippets for contributors to a release.
|
||||
|
||||
Usage:
|
||||
python {sys.argv[0]} {sys.argv[0]} <RELEASE>
|
||||
''')
|
||||
sys.exit(ret)
|
329
docs/contributors/people.json
Normal file
329
docs/contributors/people.json
Normal file
@ -0,0 +1,329 @@
|
||||
{
|
||||
"Almad": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "Almad",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": "almadcz"
|
||||
},
|
||||
"Annette Wilson": {
|
||||
"committed": [],
|
||||
"github": "annettejanewilson",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Anton Emelyanov": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "king-menin",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"D8ger": {
|
||||
"committed": [],
|
||||
"github": "caofanCPU",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Dave": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "davecheney",
|
||||
"reported": [],
|
||||
"twitter": "davecheney"
|
||||
},
|
||||
"Dawid Ferenczy Rogo\u017ean": {
|
||||
"committed": [],
|
||||
"github": "ferenczy",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": "DawidFerenczy"
|
||||
},
|
||||
"Elena Lape": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "elenalape",
|
||||
"reported": [],
|
||||
"twitter": "elena_lape"
|
||||
},
|
||||
"Fabio Peruzzo": {
|
||||
"committed": [],
|
||||
"github": "peruzzof",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"F\u00fash\u0113ng": {
|
||||
"committed": [],
|
||||
"github": "lienide",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Giampaolo Rodola": {
|
||||
"committed": [],
|
||||
"github": "giampaolo",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Hugh Williams": {
|
||||
"committed": [],
|
||||
"github": "hughpv",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Ilya Sukhanov": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "IlyaSukhanov",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Jakub Roztocil": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "jakubroztocil",
|
||||
"reported": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": "jakubroztocil"
|
||||
},
|
||||
"Jan Verbeek": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "blyxxyz",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Jannik Vieten": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "exploide",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Marcel St\u00f6r": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "marcelstoer",
|
||||
"reported": [],
|
||||
"twitter": "frightanic"
|
||||
},
|
||||
"Mariano Ruiz": {
|
||||
"committed": [],
|
||||
"github": "mrsarm",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": "mrsarm82"
|
||||
},
|
||||
"Micka\u00ebl Schoentgen": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "BoboTiG",
|
||||
"reported": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": "__tiger222__"
|
||||
},
|
||||
"Miro Hron\u010dok": {
|
||||
"committed": [
|
||||
"2.5.0",
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "hroncok",
|
||||
"reported": [],
|
||||
"twitter": "hroncok"
|
||||
},
|
||||
"Mohamed Daahir": {
|
||||
"committed": [],
|
||||
"github": "ducaale",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Omer Akram": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "om26er",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": "om26er"
|
||||
},
|
||||
"Pavel Alexeev aka Pahan-Hubbitus": {
|
||||
"committed": [],
|
||||
"github": "Hubbitus",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Samuel Marks": {
|
||||
"committed": [],
|
||||
"github": "SamuelMarks",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Sullivan SENECHAL": {
|
||||
"committed": [],
|
||||
"github": "soullivaneuh",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Thomas Klinger": {
|
||||
"committed": [],
|
||||
"github": "mosesontheweb",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Vincent van \u2019t Zand": {
|
||||
"committed": [],
|
||||
"github": "vovtz",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Yannic Schneider": {
|
||||
"committed": [],
|
||||
"github": "cynay",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"a1346054": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "a1346054",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"bl-ue": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "FiReBlUe45",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"claudiatd": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "claudiatd",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"dkreeft": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "dkreeft",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"henryhu712": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "henryhu712",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"jakubroztocil": {
|
||||
"committed": [
|
||||
"2.6.0"
|
||||
],
|
||||
"github": "jkbr",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"jungle-boogie": {
|
||||
"committed": [],
|
||||
"github": "jungle-boogie",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"nixbytes": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "nixbytes",
|
||||
"reported": [],
|
||||
"twitter": "linuxbyte3"
|
||||
},
|
||||
"qiulang": {
|
||||
"committed": [],
|
||||
"github": "qiulang",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zwx00": {
|
||||
"committed": [],
|
||||
"github": "zwx00",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"\u5d14\u5c0f\u4e8c": {
|
||||
"committed": [],
|
||||
"github": "rogerdehe",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"\u9ec4\u6d77": {
|
||||
"committed": [],
|
||||
"github": "hh-in-zhuzhou",
|
||||
"reported": [
|
||||
"2.6.0"
|
||||
],
|
||||
"twitter": null
|
||||
}
|
||||
}
|
13
docs/contributors/snippet.jinja2
Normal file
13
docs/contributors/snippet.jinja2
Normal file
@ -0,0 +1,13 @@
|
||||
<!-- Blog post -->
|
||||
|
||||
## 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 %}.
|
||||
|
||||
<!-- Twitter -->
|
||||
|
||||
We’d like to thank these amazing people for their contributions to HTTPie {{ release }}: {% for name, details in contributors.items() if details.twitter -%}
|
||||
@{{ details.twitter }}{{ '' if loop.last else ', ' }}
|
||||
{%- endfor %} 🥧
|
5
docs/installation/README.md
Normal file
5
docs/installation/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
Here we maintain a database of installation methods, from which we generate
|
||||
the installation section in docs. If you’d like add or update an installation method,
|
||||
edit [methods.yml](./methods.yml), do not edit the main docs directly.
|
||||
|
||||
For HTTPie installation instructions see: <https://httpie.io/docs#installation>.
|
85
docs/installation/generate.py
Normal file
85
docs/installation/generate.py
Normal file
@ -0,0 +1,85 @@
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
Database = Dict[str, dict]
|
||||
|
||||
# Files
|
||||
HERE = Path(__file__).parent
|
||||
DB_FILE = HERE / 'methods.yml'
|
||||
DOC_FILE = HERE.parent / 'README.md'
|
||||
TPL_FILE = HERE / 'installation.jinja2'
|
||||
|
||||
# Database keys
|
||||
KEY_DOC_STRUCTURE = 'docs-structure'
|
||||
KEY_TOOLS = 'tools'
|
||||
|
||||
# Markers in-between content will be put.
|
||||
MARKER_START = '<div data-installation-instructions>'
|
||||
MARKER_END = '</div>'
|
||||
|
||||
|
||||
def generate_documentation() -> str:
|
||||
database = load_database()
|
||||
structure = build_docs_structure(database)
|
||||
template = Template(source=TPL_FILE.read_text(encoding='utf-8'))
|
||||
output = template.render(structure=structure)
|
||||
output = clean_template_output(output)
|
||||
return output
|
||||
|
||||
|
||||
def save_doc_file(content: str) -> None:
|
||||
current_doc = load_doc_file()
|
||||
marker_start = current_doc.find(MARKER_START) + len(MARKER_START)
|
||||
assert marker_start > 0, 'cannot find the start marker'
|
||||
marker_end = current_doc.find(MARKER_END, marker_start)
|
||||
assert marker_start < marker_end, f'{marker_end=} < {marker_start=}'
|
||||
updated_doc = (
|
||||
current_doc[:marker_start]
|
||||
+ '\n\n'
|
||||
+ content
|
||||
+ '\n\n'
|
||||
+ current_doc[marker_end:]
|
||||
)
|
||||
if current_doc != updated_doc:
|
||||
DOC_FILE.write_text(updated_doc, encoding='utf-8')
|
||||
|
||||
|
||||
def build_docs_structure(database: Database):
|
||||
tools = database[KEY_TOOLS]
|
||||
assert len(tools) == len({tool['title'] for tool in tools.values()}), 'tool titles need to be unique'
|
||||
tree = database[KEY_DOC_STRUCTURE]
|
||||
structure = []
|
||||
for platform, tools_ids in tree.items():
|
||||
assert platform.isalnum(), f'{platform=} must be alpha-numeric for generated links to work'
|
||||
platform_tools = [tools[tool_id] for tool_id in tools_ids]
|
||||
structure.append((platform, platform_tools))
|
||||
return structure
|
||||
|
||||
|
||||
def clean_template_output(output):
|
||||
output = '\n'.join(line.strip() for line in output.strip().splitlines())
|
||||
output = re.sub('\n{3,}', '\n\n', output)
|
||||
return output
|
||||
|
||||
|
||||
def load_database() -> Database:
|
||||
return yaml.safe_load(DB_FILE.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def load_doc_file() -> str:
|
||||
return DOC_FILE.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
content = generate_documentation()
|
||||
save_doc_file(content)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
37
docs/installation/installation.jinja2
Normal file
37
docs/installation/installation.jinja2
Normal file
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
THE INSTALLATION SECTION IS GENERATED
|
||||
|
||||
Do not edit here, but in docs/installation/.
|
||||
|
||||
-->
|
||||
{% for platform, tools in structure %}
|
||||
- [{{ platform }}](#{{ platform.lower() }}){% endfor %} {# <= keep `endfor` here to prevent unwanted `\n` #}
|
||||
|
||||
{% for platform, tools in structure %}
|
||||
|
||||
### {{ platform }}
|
||||
|
||||
{% for tool in tools %}
|
||||
#### {{ tool.title }}
|
||||
|
||||
{% if tool.note %}
|
||||
{{ tool.note }}
|
||||
{% endif %}
|
||||
|
||||
{% if tool.links.setup %}
|
||||
To install [{{ tool.name }}]({{ tool.links.homepage }}), see [its installation]({{ tool.links.setup }}).
|
||||
{% endif %}
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ {{ tool.commands.install|join('\n$ ') }}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ {{ tool.commands.upgrade|join('\n$ ') }}
|
||||
```
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
<!-- /GENERATED SECTION -->
|
181
docs/installation/methods.yml
Normal file
181
docs/installation/methods.yml
Normal file
@ -0,0 +1,181 @@
|
||||
# Database of HTTPie installation methods. Used to build the docs.
|
||||
#
|
||||
# We currently only include here methods for popular systems where we take care of the package,
|
||||
# or have a good relationship with the maintainers.
|
||||
#
|
||||
# Each tool name should be unique (it becomes a linkable header).
|
||||
# If a tools have `links.setup`, it also needs `links.homepage`.
|
||||
# Some tools are available on multiple platforms, take into account when editing.
|
||||
#
|
||||
|
||||
docs-structure:
|
||||
Universal:
|
||||
- pypi
|
||||
macOS:
|
||||
- brew-mac
|
||||
- port
|
||||
Windows:
|
||||
- chocolatey
|
||||
Linux:
|
||||
- snap-linux
|
||||
- brew-linux
|
||||
- apt
|
||||
- dnf
|
||||
- yum
|
||||
- pacman
|
||||
FreeBSD:
|
||||
- pkg
|
||||
|
||||
tools:
|
||||
apt:
|
||||
title: Debian and Ubuntu
|
||||
note: Also works for other 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.
|
||||
name: APT
|
||||
links:
|
||||
homepage: https://en.wikipedia.org/wiki/APT_(software)
|
||||
package: https://packages.debian.org/sid/web/httpie
|
||||
commands:
|
||||
install:
|
||||
- apt update
|
||||
- apt install httpie
|
||||
upgrade:
|
||||
- apt update
|
||||
- apt upgrade httpie
|
||||
|
||||
brew-mac:
|
||||
title: Homebrew
|
||||
name: Homebrew
|
||||
links:
|
||||
homepage: https://brew.sh/
|
||||
setup: https://docs.brew.sh/Installation
|
||||
package: https://formulae.brew.sh/formula/httpie
|
||||
commands:
|
||||
install:
|
||||
- brew update
|
||||
- brew install httpie
|
||||
upgrade:
|
||||
- brew update
|
||||
- brew upgrade httpie
|
||||
|
||||
brew-linux:
|
||||
title: Linuxbrew
|
||||
name: Linuxbrew
|
||||
links:
|
||||
homepage: https://docs.brew.sh/Homebrew-on-Linux
|
||||
setup: https://docs.brew.sh/Homebrew-on-Linux#install
|
||||
package: https://formulae.brew.sh/formula/httpie
|
||||
commands:
|
||||
install:
|
||||
- brew update
|
||||
- brew install httpie
|
||||
upgrade:
|
||||
- brew update
|
||||
- brew upgrade httpie
|
||||
|
||||
chocolatey:
|
||||
title: Chocolatey
|
||||
name: Chocolatey
|
||||
links:
|
||||
homepage: https://chocolatey.org/
|
||||
setup: https://chocolatey.org/install
|
||||
package: https://community.chocolatey.org/packages/httpie/
|
||||
commands:
|
||||
install:
|
||||
- choco install httpie
|
||||
upgrade:
|
||||
- choco upgrade httpie
|
||||
|
||||
dnf:
|
||||
title: Fedora
|
||||
name: DNF
|
||||
links:
|
||||
homepage: https://fedoraproject.org/wiki/DNF
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- dnf install httpie
|
||||
upgrade:
|
||||
- dnf upgrade httpie
|
||||
|
||||
pacman:
|
||||
title: Arch Linux
|
||||
name: pacman
|
||||
note: Also works for other Arch-derived distributions like ArcoLinux, EndeavourOS, Artix Linux, etc.
|
||||
links:
|
||||
homepage: https://archlinux.org/pacman/
|
||||
package: https://archlinux.org/packages/community/any/httpie/
|
||||
commands:
|
||||
install:
|
||||
- pacman -Sy httpie
|
||||
upgrade:
|
||||
- pacman -Syu httpie
|
||||
|
||||
pkg:
|
||||
title: FreshPorts
|
||||
name: FreshPorts
|
||||
links:
|
||||
homepage: https://www.freebsd.org/cgi/man.cgi?query=pkg&sektion=8&n=1
|
||||
package: https://www.freshports.org/www/py-httpie/
|
||||
commands:
|
||||
install:
|
||||
- pkg install www/py-httpie
|
||||
upgrade:
|
||||
- pkg upgrade www/py-httpie
|
||||
|
||||
port:
|
||||
title: MacPorts
|
||||
name: MacPorts
|
||||
links:
|
||||
homepage: https://www.macports.org/
|
||||
setup: https://www.macports.org/install.php
|
||||
package: https://ports.macports.org/port/httpie/
|
||||
commands:
|
||||
install:
|
||||
- port selfupdate
|
||||
- port install httpie
|
||||
upgrade:
|
||||
- port selfupdate
|
||||
- port upgrade httpie
|
||||
|
||||
pypi:
|
||||
title: PyPI
|
||||
name: pip
|
||||
note: Please make sure you have Python 3.7 or newer (`python --version`).
|
||||
links:
|
||||
homepage: https://pypi.org/
|
||||
# setup: https://pip.pypa.io/en/stable/installation/
|
||||
package: https://pypi.org/project/httpie/
|
||||
commands:
|
||||
install:
|
||||
- python -m pip install --upgrade pip wheel
|
||||
- python -m pip install httpie
|
||||
upgrade:
|
||||
- python -m pip install --upgrade pip wheel
|
||||
- python -m pip install --upgrade httpie
|
||||
|
||||
snap-linux:
|
||||
title: Snapcraft (Linux)
|
||||
name: Snapcraft
|
||||
links:
|
||||
homepage: https://snapcraft.io/
|
||||
setup: https://snapcraft.io/docs/installing-snapd
|
||||
package: https://snapcraft.io/httpie
|
||||
commands:
|
||||
install:
|
||||
- snap install httpie
|
||||
upgrade:
|
||||
- snap refresh httpie
|
||||
|
||||
yum:
|
||||
title: CentOS and RHEL
|
||||
name: Yum
|
||||
note: Also works for other RHEL-derived distributions like ClearOS, Oracle Linux, etc.
|
||||
links:
|
||||
homepage: http://yum.baseurl.org/
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- yum install epel-release
|
||||
- yum install httpie
|
||||
upgrade:
|
||||
- yum upgrade httpie
|
44
docs/markdownlint.rb
Normal file
44
docs/markdownlint.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# Rules for <https://github.com/markdownlint/markdownlint>
|
||||
|
||||
# Load all rules by default
|
||||
all
|
||||
|
||||
#
|
||||
# Tweak rules
|
||||
#
|
||||
|
||||
# MD002 First header should be a top level header
|
||||
# Because we use HTML to hide them on the website.
|
||||
exclude_rule 'MD002'
|
||||
|
||||
# MD013 Line length
|
||||
exclude_rule 'MD013'
|
||||
|
||||
# MD014 Dollar signs used before commands without showing output
|
||||
exclude_rule 'MD014'
|
||||
|
||||
# MD028 Blank line inside blockquote
|
||||
exclude_rule 'MD028'
|
||||
|
||||
# Tell the linter to use ordered lists:
|
||||
# 1. Foo
|
||||
# 2. Bar
|
||||
# 3. Baz
|
||||
#
|
||||
# Instead of:
|
||||
# 1. Foo
|
||||
# 1. Bar
|
||||
# 1. Baz
|
||||
rule 'MD029', :style => :ordered
|
||||
|
||||
# MD033 Inline HTML
|
||||
# TODO: Tweak elements when https://github.com/markdownlint/markdownlint/issues/118 will be done?
|
||||
exclude_rule 'MD033'
|
||||
|
||||
# MD034 Bare URL used
|
||||
# TODO: Remove when https://github.com/markdownlint/markdownlint/issues/328 will be fixed.
|
||||
exclude_rule 'MD034'
|
||||
|
||||
# MD041 First line in file should be a top level header
|
||||
# Because we use HTML to hide them on the website.
|
||||
exclude_rule 'MD041'
|
46
docs/packaging/README.md
Normal file
46
docs/packaging/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# HTTPie release process
|
||||
|
||||
Welcome on the documentation part of the **HTTPie release process**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions, then you can find all you need for your OS on [that page](https://httpie.io/docs#installation). In the case you do not find your OS, [let us know](https://github.com/httpie/httpie/issues/).
|
||||
- If you are looking for technical information about the HTTPie packaging, then you are at the good place.
|
||||
|
||||
## About
|
||||
|
||||
You are looking at the HTTPie packaging documentation, where you will find valuable information about how we manage to release HTTPie to lots of OSes, including technical data that may be worth reading if you are a package maintainer.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
- 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).
|
||||
- Update the HTTPie version bundled into [Termible](https://termible.io/) ([example](https://github.com/httpie/termible/pull/1)).
|
||||
|
||||
## Finally, spread dowstream
|
||||
|
||||
Find out how we do release new versions for each and every supported OS in the following table.
|
||||
A more complete state of deployment can be found on [repology](https://repology.org/project/httpie/versions), including unofficial packages.
|
||||
|
||||
| OS | Maintainer |
|
||||
| -------------------------------------------: | -------------- |
|
||||
| [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** |
|
||||
| [Snapcraft](snapcraft/) | **HTTPie** |
|
||||
| [Windows — Chocolatey](windows-chocolatey/) | **HTTPie** |
|
||||
|
||||
:new: You do not find your system or you would like to see HTTPie supported on another OS? Then [let us know](https://github.com/httpie/httpie/issues/).
|
33
docs/packaging/brew/README.md
Normal file
33
docs/packaging/brew/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# HTTPie on Homebrew, and Linuxbrew
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Homebrew**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Homebrew, then you can find them on [that page](https://httpie.io/docs#homebrew) ([that one](https://httpie.io/docs#linuxbrew) for Linuxbrew).
|
||||
- If you are looking for technical information about the HTTPie packaging on Homebrew, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Homebrew. They apply to Linuxbrew as well.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
:construction: Work in progress.
|
||||
|
||||
First, update the current Formula:
|
||||
|
||||
```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'
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
@ -15,21 +15,23 @@ 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': '2.10',
|
||||
'idna': '3.2',
|
||||
}
|
||||
|
||||
|
||||
# Note: Keep that list sorted.
|
||||
PACKAGES = [
|
||||
'certifi',
|
||||
'charset-normalizer',
|
||||
'defusedxml',
|
||||
'httpie',
|
||||
'idna',
|
||||
'Pygments',
|
||||
'PySocks',
|
||||
'requests',
|
||||
'requests-toolbelt',
|
||||
'certifi',
|
||||
'urllib3',
|
||||
'idna',
|
||||
'chardet',
|
||||
'PySocks',
|
||||
'defusedxml',
|
||||
'multidict',
|
||||
]
|
||||
|
||||
|
86
docs/packaging/brew/httpie.rb
Normal file
86
docs/packaging/brew/httpie.rb
Normal file
@ -0,0 +1,86 @@
|
||||
class Httpie < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
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"
|
||||
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"
|
||||
end
|
||||
|
||||
depends_on "python@3.10"
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz"
|
||||
sha256 "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"
|
||||
end
|
||||
|
||||
resource "charset-normalizer" do
|
||||
url "https://files.pythonhosted.org/packages/48/44/76b179e0d1afe6e6a91fd5661c284f60238987f3b42b676d141d01cd5b97/charset-normalizer-2.0.10.tar.gz"
|
||||
sha256 "876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"
|
||||
end
|
||||
|
||||
resource "defusedxml" do
|
||||
url "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz"
|
||||
sha256 "1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz"
|
||||
sha256 "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
end
|
||||
|
||||
resource "Pygments" do
|
||||
url "https://files.pythonhosted.org/packages/94/9c/cb656d06950268155f46d4f6ce25d7ffc51a0da47eadf1b164bbf23b718b/Pygments-2.11.2.tar.gz"
|
||||
sha256 "4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
url "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz"
|
||||
sha256 "3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz"
|
||||
sha256 "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"
|
||||
end
|
||||
|
||||
resource "requests-toolbelt" do
|
||||
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
|
||||
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/b0/b1/7bbf5181f8e3258efae31702f5eab87d8a74a72a0aa78bc8c08c1466e243/urllib3-1.26.8.tar.gz"
|
||||
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")
|
||||
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
end
|
||||
end
|
47
docs/packaging/linux-arch/PKGBUILD
Normal file
47
docs/packaging/linux-arch/PKGBUILD
Normal file
@ -0,0 +1,47 @@
|
||||
# Maintainer: Jelle van der Waa <jelle@archlinux.org>
|
||||
# Maintainer: daurnimator <daurnimator@archlinux.org>
|
||||
# Contributor: Daniel Micay <danielmicay@gmail.com>
|
||||
# Contributor: Thomas Weißschuh <thomas_weissschuh lavabit com>
|
||||
|
||||
pkgname=httpie
|
||||
pkgver=2.6.0
|
||||
pkgrel=1
|
||||
pkgdesc="human-friendly CLI HTTP client for the API era"
|
||||
url="https://github.com/httpie/httpie"
|
||||
depends=('python-defusedxml'
|
||||
'python-pygments'
|
||||
'python-pysocks'
|
||||
'python-requests'
|
||||
'python-requests-toolbelt'
|
||||
'python-charset-normalizer')
|
||||
makedepends=('python-setuptools')
|
||||
checkdepends=('python-pytest'
|
||||
'python-pytest-httpbin'
|
||||
'python-responses')
|
||||
conflicts=(python-httpie)
|
||||
replaces=(python-httpie python2-httpie)
|
||||
license=('BSD')
|
||||
arch=('any')
|
||||
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
|
||||
sha256sums=('3bcd9a8cb2b11299da12d3af36c095c6d4b665e41c395898a07f1ae4d99fc14a')
|
||||
|
||||
build() {
|
||||
cd $pkgname-$pkgver
|
||||
python3 setup.py build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $pkgname-$pkgver
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/httpie/LICENSE"
|
||||
python3 setup.py install --root="$pkgdir" --optimize=1
|
||||
|
||||
# Fix upstream, include them in MANIFEST.in and use data_files in setup.py to install them automatically
|
||||
# TODO: add zsh support
|
||||
install -Dm644 extras/httpie-completion.bash "$pkgdir"/usr/share/bash-completion/completions/http
|
||||
install -Dm644 extras/httpie-completion.fish "$pkgdir"/usr/share/fish/vendor_completions.d/http.fish
|
||||
}
|
||||
|
||||
check() {
|
||||
cd $pkgname-$pkgver
|
||||
PYTHONDONTWRITEBYTECODE=1 pytest tests
|
||||
}
|
22
docs/packaging/linux-arch/README.md
Normal file
22
docs/packaging/linux-arch/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# HTTPie on Arch Linux, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Arch Linux**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Arch Linux, then you can find them on [that page](https://httpie.io/docs#arch-linux).
|
||||
- If you are looking for technical information about the HTTPie packaging on Arch Linux, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Arch Linux. They apply to Arch-derived distributions as well, like ArcoLinux, EndeavourOS, Artix Linux, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
Note: Sending patches downstream does not seem easy. We failed to find where is located the package file on <https://gitlab.archlinux.org>. So we are relying on the last maintainer, daurnimator, and it works pretty well so far.
|
||||
|
||||
Check <https://archlinux.org/packages/community/any/httpie/> and if the version is outdated, simply [report it](https://archlinux.org/packages/community/any/httpie/flag/).
|
||||
|
||||
## Hacking
|
||||
|
||||
Left blank on purpose, we will fill that section when we will have access to the downstream repository.
|
26
docs/packaging/linux-centos/README.md
Normal file
26
docs/packaging/linux-centos/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# HTTPie on CentOS, RHEL, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for CentOS and RHEL**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on CentOS, then you can find them on [that page](https://httpie.io/docs#centos-and-rhel).
|
||||
- If you are looking for technical information about the HTTPie packaging on CentOS, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for CentOS. They apply to RHEL as well, and any RHEL-derived distributions like ClearOS, Oracle Linux, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is [Mikel Olasagasti](https://github.com/kaxero).
|
||||
|
||||
## Overall process
|
||||
|
||||
Same as [Fedora](../linux-fedora/README.md#overall-process).
|
||||
|
||||
## Q/A with Mikel
|
||||
|
||||
Q: What should we do to help seeing a new version on CentOS?
|
||||
|
||||
A: When a new release is published Miro and I get notified by [release-monitoring](https://release-monitoring.org/project/1337/), that fills a BugZilla ticket reporting a new version being available.
|
||||
|
||||
The system also tries to create a simple patch to update the spec file, but in the case of CentOS it needs some manual revision. For example for 2.5.0 `defuxedxml` dep is required. Maybe with CentOS-9 and some new macros that are available now in Fedora it can be automated same way. But even the bump can be automated, maintainers should check for license changes, new binaries/docs/ and so on.
|
29
docs/packaging/linux-debian/README.md
Normal file
29
docs/packaging/linux-debian/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# HTTPie on Debian, Ubuntu, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Debian GNU/Linux**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Debian GNU/Linux, then you can find them on [that page](https://httpie.io/docs#debian-and-ubuntu).
|
||||
- If you are looking for technical information about the HTTPie packaging on Debian GNU/Linux, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
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.
|
||||
|
||||
## Overall process
|
||||
|
||||
Open a new bug on the Debian Bug Tracking System by sending an email:
|
||||
|
||||
- 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>
|
||||
```
|
48
docs/packaging/linux-fedora/README.md
Normal file
48
docs/packaging/linux-fedora/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# HTTPie on Fedora
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Fedora**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Fedora, then you can find them on [that page](https://httpie.io/docs#fedora).
|
||||
- If you are looking for technical information about the HTTPie packaging on Fedora, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Fedora.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is [Miro Hrončok](https://github.com/hroncok).
|
||||
|
||||
## Overall process
|
||||
|
||||
We added the [.packit.yaml](https://github.com/httpie/httpie/blob/master/.packit.yaml) local file.
|
||||
It unlocks real-time Fedora checks on pull requests and new releases.
|
||||
|
||||
So there is nothing to do on our side: `Packit` will see the new release and open a pull request [there](https://src.fedoraproject.org/rpms/httpie). Then, the Fedora maintainer will review and merge.
|
||||
|
||||
It is also possible to follow [user feedbacks](https://bodhi.fedoraproject.org/updates/?packages=httpie) for all builds.
|
||||
|
||||
## Q/A with Miro
|
||||
|
||||
Q: What would the command to install the latest stable version look like?
|
||||
|
||||
A: Assuming the latest stable version is already propagated to Fedora:
|
||||
|
||||
```bash
|
||||
# Note that yum is an alias to dnf.
|
||||
$ sudo dnf install httpie
|
||||
```
|
||||
|
||||
Q: Will dnf/yum upgrade then update to the latest?
|
||||
|
||||
A: Yes, assuming the same as above.
|
||||
|
||||
Q: Are new versions backported automatically?
|
||||
|
||||
A: No. The process is:
|
||||
|
||||
1. A new HTTPie release is created on Github.
|
||||
2. A pull request for Fedora `rawhide` (the development version of Fedora, currently Fedora 36) is created.
|
||||
3. A Fedora packager (usually Miro) sanity checks the pull request and merges, builds. HTTPie is updated in `rawhide` within 24 hours (sometimes more, for unrelated issues).
|
||||
4. A Fedora packager decides whether the upgrade is suitable for stable Fedora releases (currently 35, 34, 33), if so, merges the changes there.
|
||||
5. (if the above is yes) The new version of HTTPie lands in `updates-testing` repo where it waits for user feedback and lands within ~1 week for broad availability.
|
251
docs/packaging/linux-fedora/httpie.spec.txt
Normal file
251
docs/packaging/linux-fedora/httpie.spec.txt
Normal file
@ -0,0 +1,251 @@
|
||||
Name: httpie
|
||||
Version: 2.6.0
|
||||
Release: 1%{?dist}
|
||||
Summary: A Curl-like tool for humans
|
||||
|
||||
License: BSD
|
||||
URL: https://httpie.org/
|
||||
Source0: https://github.com/httpie/httpie/archive/%{version}/%{name}-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: pyproject-rpm-macros
|
||||
|
||||
BuildRequires: help2man
|
||||
|
||||
%description
|
||||
HTTPie is a CLI HTTP utility built out of frustration with existing tools. The
|
||||
goal is to make CLI interaction with HTTP-based services as human-friendly as
|
||||
possible.
|
||||
|
||||
HTTPie does so by providing an http command that allows for issuing arbitrary
|
||||
HTTP requests using a simple and natural syntax and displaying colorized
|
||||
responses.
|
||||
|
||||
|
||||
%prep
|
||||
%autosetup -p1
|
||||
|
||||
|
||||
%generate_buildrequires
|
||||
%pyproject_buildrequires -rx test
|
||||
|
||||
|
||||
%build
|
||||
%pyproject_wheel
|
||||
|
||||
|
||||
%install
|
||||
%pyproject_install
|
||||
%pyproject_save_files httpie
|
||||
|
||||
# Bash completion
|
||||
mkdir -p %{buildroot}%{_datadir}/bash-completion/completions
|
||||
cp -a extras/httpie-completion.bash %{buildroot}%{_datadir}/bash-completion/completions/http
|
||||
ln -s ./http %{buildroot}%{_datadir}/bash-completion/completions/https
|
||||
|
||||
# Fish completion
|
||||
mkdir -p %{buildroot}%{_datadir}/fish/vendor_completions.d/
|
||||
cp -a extras/httpie-completion.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/http.fish
|
||||
ln -s ./http.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/https.fish
|
||||
|
||||
|
||||
# Generate man pages for everything
|
||||
export PYTHONPATH=%{buildroot}%{python3_sitelib}
|
||||
mkdir -p %{buildroot}%{_mandir}/man1
|
||||
help2man %{buildroot}%{_bindir}/http > %{buildroot}%{_mandir}/man1/http.1
|
||||
help2man %{buildroot}%{_bindir}/https > %{buildroot}%{_mandir}/man1/https.1
|
||||
help2man %{buildroot}%{_bindir}/httpie > %{buildroot}%{_mandir}/man1/httpie.1
|
||||
|
||||
|
||||
%check
|
||||
%pytest -v
|
||||
|
||||
|
||||
%files -f %{pyproject_files}
|
||||
%doc README.md
|
||||
%license LICENSE
|
||||
%{_bindir}/http
|
||||
%{_bindir}/https
|
||||
%{_bindir}/httpie
|
||||
%{_mandir}/man1/http.1*
|
||||
%{_mandir}/man1/https.1*
|
||||
%{_mandir}/man1/httpie.1*
|
||||
# we co-own the entire directory structures for bash/fish completion to avoid a dependency
|
||||
%{_datadir}/bash-completion/
|
||||
%{_datadir}/fish/
|
||||
|
||||
|
||||
%changelog
|
||||
* Fri Oct 15 2021 Miro Hrončok <mhroncok@redhat.com> - 2.6.0-1
|
||||
- Update to 2.6.0
|
||||
- Fixes: rhbz#2014022
|
||||
|
||||
* Tue Sep 07 2021 Miro Hrončok <mhroncok@redhat.com> - 2.5.0-1
|
||||
- Update to 2.5.0
|
||||
- Fixes: rhbz#2001693
|
||||
|
||||
* Thu Jul 22 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2.4.0-4
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
|
||||
|
||||
* Fri Jun 04 2021 Python Maint <python-maint@redhat.com> - 2.4.0-3
|
||||
- Rebuilt for Python 3.10
|
||||
|
||||
* Thu May 27 2021 Miro Hrončok <mhroncok@redhat.com> - 2.4.0-2
|
||||
- Add Bash and Fish completion
|
||||
- Fixes rhbz#1834441
|
||||
- Run tests on build time
|
||||
|
||||
* Wed Mar 24 2021 Mikel Olasagasti Uranga <mikel@olasagasti.info> - 2.4.0-1
|
||||
- Update to 2.4.0
|
||||
- Use pypi_source macro
|
||||
|
||||
* Tue Jan 26 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2.3.0-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
|
||||
|
||||
* Thu Jan 21 2021 Nils Philippsen <nils@tiptoe.de> - 2.3.0-2
|
||||
- use macros for Python dependencies
|
||||
- add missing Python dependencies needed for running help2man
|
||||
- remove manual Python dependencies
|
||||
- discard stderr when running help2man
|
||||
|
||||
* Thu Dec 24 2020 Nils Philippsen <nils@tiptoe.de> - 2.3.0-1
|
||||
- version 2.3.0
|
||||
- Python 2 is no more
|
||||
- use %%autosetup and Python build macros
|
||||
- remove EL7-isms
|
||||
- explicitly require sed for building
|
||||
|
||||
* Tue Jul 28 2020 Fedora Release Engineering <releng@fedoraproject.org> - 1.0.3-4
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild
|
||||
|
||||
* Tue May 26 2020 Miro Hrončok <mhroncok@redhat.com> - 1.0.3-3
|
||||
- Rebuilt for Python 3.9
|
||||
|
||||
* Wed Jan 29 2020 Fedora Release Engineering <releng@fedoraproject.org> - 1.0.3-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
|
||||
|
||||
* Mon Sep 30 2019 Rick Elrod <relrod@redhat.com> - 1.0.3-1
|
||||
- Latest upstream
|
||||
|
||||
* Mon Aug 19 2019 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-15
|
||||
- Rebuilt for Python 3.8
|
||||
|
||||
* Thu Jul 25 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-14
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
|
||||
|
||||
* Fri Feb 01 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-13
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
|
||||
|
||||
* Fri Jul 13 2018 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-12
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
|
||||
|
||||
* Tue Jun 19 2018 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-11
|
||||
- Rebuilt for Python 3.7
|
||||
|
||||
* Wed Feb 07 2018 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-10
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
|
||||
|
||||
* Wed Jul 26 2017 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-9
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
|
||||
|
||||
* Fri Mar 10 2017 Ralph Bean <rbean@redhat.com> - 0.9.4-8
|
||||
- Fix help2man usage with python3.
|
||||
https://bugzilla.redhat.com/show_bug.cgi?id=1430733
|
||||
|
||||
* Mon Feb 27 2017 Ralph Bean <rbean@redhat.com> - 0.9.4-7
|
||||
- Fix missing Requires. https://bugzilla.redhat.com/show_bug.cgi?id=1417730
|
||||
|
||||
* Fri Feb 10 2017 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-6
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
|
||||
|
||||
* Mon Jan 2 2017 Ricky Elrod <relrod@redhat.com> - 0.9.4-5
|
||||
- Add missing Obsoletes.
|
||||
|
||||
* Mon Jan 2 2017 Ricky Elrod <relrod@redhat.com> - 0.9.4-4
|
||||
- Nuke python-version-specific subpackages. Just use py3 if we can.
|
||||
|
||||
* Mon Dec 19 2016 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-3
|
||||
- Rebuild for Python 3.6
|
||||
|
||||
* Tue Jul 19 2016 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.4-2
|
||||
- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages
|
||||
|
||||
* Tue Jul 05 2016 Ricky Elrod <relrod@redhat.com> - 0.9.4-1
|
||||
- Update to latest upstream.
|
||||
|
||||
* Fri Jun 03 2016 Ricky Elrod <relrod@redhat.com> - 0.9.3-4
|
||||
- Add proper Obsoletes for rhbz#1329226.
|
||||
|
||||
* Wed Feb 03 2016 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.3-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild
|
||||
|
||||
* Mon Jan 04 2016 Ralph Bean <rbean@redhat.com> - 0.9.3-2
|
||||
- Modernize python macros and subpackaging.
|
||||
- Move LICENSE to %%license macro.
|
||||
- Make python3 the default on modern Fedora.
|
||||
|
||||
* Mon Jan 04 2016 Ralph Bean <rbean@redhat.com> - 0.9.3-1
|
||||
- new version
|
||||
|
||||
* Tue Nov 10 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.2-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Changes/python3.5
|
||||
|
||||
* Wed Jun 17 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.2-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
|
||||
|
||||
* Thu Mar 26 2015 Ricky Elrod <relrod@redhat.com> - 0.9.2-1
|
||||
- Latest upstream release.
|
||||
|
||||
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.8.0-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
|
||||
|
||||
* Wed May 28 2014 Kalev Lember <kalevlember@gmail.com> - 0.8.0-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Changes/Python_3.4
|
||||
|
||||
* Fri Jan 31 2014 Ricky Elrod <codeblock@fedoraproject.org> - 0.8.0-1
|
||||
- Latest upstream release.
|
||||
|
||||
* Fri Oct 4 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.7.2-2
|
||||
- Add in patch to work without having python-requests 2.0.0.
|
||||
|
||||
* Sat Sep 28 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.7.2-1
|
||||
- Latest upstream release.
|
||||
|
||||
* Thu Sep 5 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-7
|
||||
- Only try building the manpage on Fedora, since RHEL's help2man doesn't
|
||||
have the --no-discard-stderr flag.
|
||||
|
||||
* Thu Sep 5 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-6
|
||||
- Loosen the requirement on python-pygments.
|
||||
|
||||
* Sat Aug 03 2013 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.6.0-5
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild
|
||||
|
||||
* Tue Jul 2 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-4
|
||||
- python-requests 1.2.3 exists in rawhide now.
|
||||
|
||||
* Sun Jun 30 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-3
|
||||
- Patch to use python-requests 1.1.0 for now.
|
||||
|
||||
* Sat Jun 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-2
|
||||
- Update to latest upstream release.
|
||||
|
||||
* Mon Apr 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.5.0-2
|
||||
- Fix changelog messup.
|
||||
|
||||
* Mon Apr 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.5.0-1
|
||||
- Update to latest upstream release.
|
||||
|
||||
* Mon Apr 8 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-3
|
||||
- Fix manpage generation by exporting PYTHONPATH.
|
||||
|
||||
* Tue Mar 26 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-2
|
||||
- Include Python3 support, and fix other review blockers.
|
||||
|
||||
* Mon Mar 11 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-1
|
||||
- Update to latest upstream release
|
||||
|
||||
* Thu Jul 19 2012 Ricky Elrod <codeblock@fedoraproject.org> - 0.2.5-1
|
||||
- Initial build.
|
50
docs/packaging/mac-ports/Portfile
Normal file
50
docs/packaging/mac-ports/Portfile
Normal file
@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8::et:sw=4:ts=4:sts=4
|
||||
|
||||
PortSystem 1.0
|
||||
PortGroup github 1.0
|
||||
PortGroup python 1.0
|
||||
|
||||
github.setup httpie httpie 2.6.0
|
||||
|
||||
maintainers {g5pw @g5pw} openmaintainer
|
||||
categories net
|
||||
description Modern, user-friendly command-line HTTP client for the API era
|
||||
long_description HTTPie (pronounced aych-tee-tee-pie) is a command line HTTP \
|
||||
client. Its goal is to make CLI interaction with web \
|
||||
services as human-friendly as possible. It provides a simple \
|
||||
http command that allows for sending arbitrary HTTP requests \
|
||||
using a simple and natural syntax, and displays colorized \
|
||||
responses. HTTPie can be used for testing, debugging, and \
|
||||
generally interacting with HTTP servers.
|
||||
platforms darwin
|
||||
license BSD
|
||||
homepage https://httpie.io/
|
||||
|
||||
variant python37 conflicts python36 python38 python39 python310 description "Use Python 3.7" {}
|
||||
variant python38 conflicts python36 python37 python39 python310 description "Use Python 3.8" {}
|
||||
variant python39 conflicts python36 python37 python38 python310 description "Use Python 3.9" {}
|
||||
variant python310 conflicts python36 python37 python38 python39 description "Use Python 3.10" {}
|
||||
|
||||
if {[variant_isset python37]} {
|
||||
python.default_version 37
|
||||
} elseif {[variant_isset python39]} {
|
||||
python.default_version 39
|
||||
} elseif {[variant_isset python310]} {
|
||||
python.default_version 310
|
||||
} else {
|
||||
default_variants +python38
|
||||
python.default_version 38
|
||||
}
|
||||
|
||||
depends_lib-append port:py${python.version}-requests \
|
||||
port:py${python.version}-requests-toolbelt \
|
||||
port:py${python.version}-pygments \
|
||||
port:py${python.version}-socks \
|
||||
port:py${python.version}-charset-normalizer \
|
||||
port:py${python.version}-defusedxml
|
||||
|
||||
checksums rmd160 07b1d1592da1c505ed3ee4ef3b6056215e16e9ff \
|
||||
sha256 63cf104bf3552305c68a74f16494a90172b15296610a875e17918e5e36373c0b \
|
||||
size 1133491
|
||||
|
||||
python.link_binaries_suffix
|
40
docs/packaging/mac-ports/README.md
Normal file
40
docs/packaging/mac-ports/README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# HTTPie on MacPorts
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for MacPorts**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on MacPorts, then you can find them on [that page](https://httpie.io/docs#macports).
|
||||
- If you are looking for technical information about the HTTPie packaging on MacPorts, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for MacPorts.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
Open a pull request to update the [downstream file](https://github.com/macports/macports-ports/blob/master/net/httpie/Portfile) ([example](https://github.com/macports/macports-ports/pull/12583)).
|
||||
|
||||
- Here is how to calculate the size and checksums (replace `2.5.0` with the correct version):
|
||||
|
||||
```bash
|
||||
# Download the archive
|
||||
$ wget https://api.github.com/repos/httpie/httpie/tarball/2.5.0
|
||||
|
||||
# Size
|
||||
$ stat --printf="%s\n" 2.5.0
|
||||
1105185
|
||||
|
||||
# Checksums
|
||||
$ openssl dgst -rmd160 2.5.0
|
||||
RIPEMD160(2.5.0)= 88d227d52199c232c0ddf704a219d1781b1e77ee
|
||||
$ openssl dgst -sha256 2.5.0
|
||||
SHA256(2.5.0)= 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc
|
||||
```
|
||||
|
||||
- The commit message must be `httpie: update to XXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
51
docs/packaging/snapcraft/README.md
Normal file
51
docs/packaging/snapcraft/README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# HTTPie on Snapcraft
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Snapcraft**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Snapcraft, then you can find them on [that page](https://httpie.io/docs#snapcraft-linux) ([that one](https://httpie.io/docs#snapcraft-macos) for macOS).
|
||||
- If you are looking for technical information about the HTTPie packaging on Snapcraft, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Snapcraft. They apply to Snapcraft on Linux, macOS, and Windows.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## 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/).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull ubuntu/latest
|
||||
docker run -it --rm ubuntu/latest
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone --depth=1 https://github.com/httpie/httpie.git
|
||||
cd httpie
|
||||
|
||||
# Build
|
||||
export SNAPCRAFT_BUILD_ENVIRONMENT_CPU=8
|
||||
export SNAPCRAFT_BUILD_ENVIRONMENT_MEMORY=16G
|
||||
snapcraft --debug
|
||||
|
||||
# Install
|
||||
sudo snap install --dangerous httpie_XXX_amd64.snap
|
||||
|
||||
# Test
|
||||
httpie.http --version
|
||||
httpie.https --version
|
||||
# Auto-aliases cannot be tested when installing a snap outside the store.
|
||||
# http --version
|
||||
# https --version
|
||||
|
||||
# Remove
|
||||
sudo snap remove httpie
|
||||
```
|
45
docs/packaging/windows-chocolatey/README.md
Normal file
45
docs/packaging/windows-chocolatey/README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# HTTPie on Chocolatey
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Chocolatey**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Chocolatey, then you can find them on [that page](https://httpie.io/docs#chocolatey).
|
||||
- If you are looking for technical information about the HTTPie packaging on Chocolatey, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Chocolatey.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
After having successfully [built and tested](#hacking) the package, push it:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Hacking
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone --depth=1 https://github.com/httpie/httpie.git
|
||||
cd httpie/docs/packaging/windows-chocolatey
|
||||
|
||||
# Build
|
||||
choco pack
|
||||
|
||||
# Check metadata
|
||||
choco info httpie -s .
|
||||
|
||||
# Install
|
||||
choco install httpie -y -dv -s "'.;https://community.chocolatey.org/api/v2/'"
|
||||
|
||||
# Test
|
||||
http --version
|
||||
https --version
|
||||
|
||||
# Remove
|
||||
choco uninstall -y httpie
|
||||
```
|
50
docs/packaging/windows-chocolatey/httpie.nuspec
Normal file
50
docs/packaging/windows-chocolatey/httpie.nuspec
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>httpie</id>
|
||||
<version>2.6.0</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.
|
||||
It comes with JSON support, syntax highlighting, persistent sessions, wget-like downloads, plugins, and more.
|
||||
|
||||
The project's goal is to make CLI interaction with web services as human-friendly as possible. HTTPie is designed for testing, debugging, and generally interacting with APIs and HTTP servers.
|
||||
The `http` and `https` commands allow for creating and sending arbitrary HTTP requests. They use simple and natural syntax and provide formatted and colorized output.
|
||||
|
||||
Main features:
|
||||
|
||||
- Built-in JSON support
|
||||
- Colorized and formatted terminal output
|
||||
- Sensible defaults for the API era
|
||||
- Persistent sessions
|
||||
- Forms and file uploads
|
||||
- HTTPS, proxies, and authentication support
|
||||
- Support for arbitrary request data and headers
|
||||
- Wget-like downloads
|
||||
- Extensions API
|
||||
- Expressive and intuitive syntax
|
||||
- Linux, macOS, Windows, and FreeBSD support
|
||||
- All that and more in 2 simple commands: `http` + `https`
|
||||
</description>
|
||||
<title>HTTPie</title>
|
||||
<authors>HTTPie</authors>
|
||||
<owners>jakubroztocil</owners>
|
||||
<copyright>2012-2021 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>
|
||||
<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>
|
||||
<projectSourceUrl>https://github.com/httpie/httpie</projectSourceUrl>
|
||||
<docsUrl>https://httpie.io/docs</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/httpie/httpie/issues</bugTrackerUrl>
|
||||
<dependencies>
|
||||
<dependency id="python3" version="3.7" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools\**" target="tools" />
|
||||
</files>
|
||||
</package>
|
@ -0,0 +1,2 @@
|
||||
$ErrorActionPreference = 'Stop';
|
||||
py -m pip install $env:ChocolateyPackageName==$env:ChocolateyPackageVersion --disable-pip-version-check
|
@ -0,0 +1,2 @@
|
||||
$ErrorActionPreference = 'Stop';
|
||||
py -m pip uninstall -y $env:ChocolateyPackageName --disable-pip-version-check
|
@ -7,7 +7,7 @@ _http_complete() {
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o default -F _http_complete http
|
||||
complete -o default -F _http_complete http httpie.http httpie.https https
|
||||
|
||||
_http_complete_options() {
|
||||
local cur_word=$1
|
||||
|
@ -1,76 +0,0 @@
|
||||
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
|
||||
# Only useful for testing until it gets accepted by homebrew maintainers.
|
||||
# (It will need to be updated from the repo version before next release.)
|
||||
#
|
||||
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
#
|
||||
class Httpie < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.io/"
|
||||
url "https://files.pythonhosted.org/packages/17/3a/90fb6702e600f5ba7d38d147bbc0b0a1e47159e3e244737319c98c140420/httpie-2.4.0.tar.gz"
|
||||
sha256 "4d1bf5779cf6c9007351cfcaa20bd19947267dc026af09246db6006a8927d8c6"
|
||||
license "BSD-3-Clause"
|
||||
head "https://github.com/httpie/httpie.git"
|
||||
|
||||
bottle do
|
||||
rebuild 1
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "a01ce8767f6ea88eb8e7894347ba64eb29294053a8ee91eed44dfaf0ab5e7ea2"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "bdffeff349595ed3c528ed791d568e308b0877246b49e05e867143ba3415a70f"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "ba0627d70f0ee49c64677f5554881ebd56371f47d45196b6564680089ce69152"
|
||||
sha256 cellar: :any_skip_relocation, mojave: "0b87901e88bdcf53c55c5138677087b4621c5aaf1fca67b53b730d5a2fd5a40a"
|
||||
sha256 cellar: :any_skip_relocation, high_sierra: "87e7348b6fb40fd8e4f7597937952469601962189e62d321b8cb4fa421e035ef"
|
||||
end
|
||||
|
||||
depends_on "python@3.9"
|
||||
|
||||
resource "Pygments" do
|
||||
url "https://files.pythonhosted.org/packages/e1/86/8059180e8217299079d8719c6e23d674aadaba0b1939e25e0cc15dcf075b/Pygments-2.7.4.tar.gz"
|
||||
sha256 "df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/6b/47/c14abc08432ab22dc18b9892252efaf005ab44066de871e72a38d6af464b/requests-2.25.1.tar.gz"
|
||||
sha256 "27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"
|
||||
end
|
||||
|
||||
resource "requests-toolbelt" do
|
||||
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
|
||||
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
end
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/06/a9/cd1fd8ee13f73a4d4f491ee219deeeae20afefa914dfb4c130cfc9dc397a/certifi-2020.12.5.tar.gz"
|
||||
sha256 "1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/d7/8d/7ee68c6b48e1ec8d41198f694ecdc15f7596356f2ff8e6b1420300cf5db3/urllib3-1.26.3.tar.gz"
|
||||
sha256 "de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz"
|
||||
sha256 "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"
|
||||
end
|
||||
|
||||
resource "chardet" do
|
||||
url "https://files.pythonhosted.org/packages/ee/2d/9cdc2b527e127b4c9db64b86647d567985940ac3698eeabc7ffaccb4ea61/chardet-4.0.0.tar.gz"
|
||||
sha256 "0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
url "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz"
|
||||
sha256 "3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
end
|
||||
|
||||
def install
|
||||
virtualenv_install_with_resources
|
||||
end
|
||||
|
||||
test do
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
end
|
||||
end
|
202
extras/profiling/benchmarks.py
Normal file
202
extras/profiling/benchmarks.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
This file is the declaration of benchmarks for HTTPie. It
|
||||
is also used to run them with the current environment.
|
||||
|
||||
Each instance of BaseRunner class will be an individual
|
||||
benchmark. And if run without any arguments, this file
|
||||
will execute every benchmark instance and report the
|
||||
timings.
|
||||
|
||||
The benchmarks are run through 'pyperf', which allows to
|
||||
do get very precise results. For micro-benchmarks like startup,
|
||||
please run `pyperf system tune` to get even more acurrate results.
|
||||
|
||||
Examples:
|
||||
|
||||
# Run everything as usual, the default is that we do 3 warmup runs
|
||||
# and 5 actual runs.
|
||||
$ python extras/profiling/benchmarks.py
|
||||
|
||||
# For retrieving results faster, pass --fast
|
||||
$ 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
|
||||
# very useful when iterating on a benchmark
|
||||
$ python extras/profiling/benchmarks.py --debug-single-value
|
||||
|
||||
# If you want to run with a custom HTTPie command (for example with
|
||||
# and HTTPie instance installed in another virtual environment),
|
||||
# pass HTTPIE_COMMAND variable.
|
||||
$ HTTPIE_COMMAND="/my/python /my/httpie" python extras/profiling/benchmarks.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property, partial
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import ClassVar, Final, List
|
||||
|
||||
import pyperf
|
||||
|
||||
# For download benchmarks, define a set of files.
|
||||
# file: (block_size, count) => total_size = block_size * count
|
||||
PREDEFINED_FILES: Final = {'3G': (3 * 1024 ** 2, 1024)}
|
||||
|
||||
|
||||
class QuietSimpleHTTPServer(SimpleHTTPRequestHandler):
|
||||
def log_message(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def start_server():
|
||||
"""Create a server to serve local files. It will create the
|
||||
PREDEFINED_FILES through dd."""
|
||||
with TemporaryDirectory() as directory:
|
||||
for file_name, (block_size, count) in PREDEFINED_FILES.items():
|
||||
subprocess.check_call(
|
||||
[
|
||||
'dd',
|
||||
'if=/dev/zero',
|
||||
f'of={file_name}',
|
||||
f'bs={block_size}',
|
||||
f'count={count}',
|
||||
],
|
||||
cwd=directory,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
handler = partial(QuietSimpleHTTPServer, directory=directory)
|
||||
server = HTTPServer(('localhost', 0), handler)
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever)
|
||||
thread.start()
|
||||
yield '{}:{}'.format(*server.socket.getsockname())
|
||||
server.shutdown()
|
||||
thread.join(timeout=0.5)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
benchmarks: ClassVar[List[BaseRunner]] = []
|
||||
stack: ExitStack = field(default_factory=ExitStack)
|
||||
runner: pyperf.Runner = field(default_factory=pyperf.Runner)
|
||||
|
||||
def run(self) -> pyperf.BenchmarkSuite:
|
||||
results = [benchmark.run(self) for benchmark in self.benchmarks]
|
||||
return pyperf.BenchmarkSuite(results)
|
||||
|
||||
@property
|
||||
def cmd(self) -> List[str]:
|
||||
if cmd := os.getenv('HTTPIE_COMMAND'):
|
||||
return shlex.split(cmd)
|
||||
|
||||
http = os.path.join(os.path.dirname(sys.executable), 'http')
|
||||
assert os.path.exists(http)
|
||||
return [sys.executable, http]
|
||||
|
||||
@cached_property
|
||||
def server(self) -> str:
|
||||
return self.stack.enter_context(start_server())
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
self.stack.close()
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseRunner:
|
||||
"""
|
||||
An individual benchmark case. By default it has the category
|
||||
(e.g like startup or download) and a name.
|
||||
"""
|
||||
|
||||
category: str
|
||||
title: str
|
||||
|
||||
def __post_init__(self):
|
||||
Context.benchmarks.append(self)
|
||||
|
||||
def run(self, context: Context) -> pyperf.Benchmark:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f'{self.title} ({self.category})'
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandRunner(BaseRunner):
|
||||
"""
|
||||
Run a single command, and benchmark it.
|
||||
"""
|
||||
|
||||
args: List[str]
|
||||
|
||||
def run(self, context: Context) -> pyperf.Benchmark:
|
||||
return context.runner.bench_command(self.name, [*context.cmd, *self.args])
|
||||
|
||||
|
||||
@dataclass
|
||||
class DownloadRunner(BaseRunner):
|
||||
"""
|
||||
Benchmark downloading a single file from the
|
||||
remote server.
|
||||
"""
|
||||
|
||||
file_name: str
|
||||
|
||||
def run(self, context: Context) -> pyperf.Benchmark:
|
||||
return context.runner.bench_command(
|
||||
self.name,
|
||||
[
|
||||
*context.cmd,
|
||||
'--download',
|
||||
'GET',
|
||||
f'{context.server}/{self.file_name}',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
CommandRunner('startup', '`http --version`', ['--version'])
|
||||
CommandRunner('startup', '`http --offline pie.dev/get`', ['--offline', 'pie.dev/get'])
|
||||
for pretty in ['all', 'none']:
|
||||
CommandRunner(
|
||||
'startup',
|
||||
f'`http --pretty={pretty} pie.dev/stream/1000`',
|
||||
[
|
||||
'--print=HBhb',
|
||||
f'--pretty={pretty}',
|
||||
'httpbin.org/stream/1000'
|
||||
]
|
||||
)
|
||||
DownloadRunner('download', '`http --download :/big_file.txt` (3GB)', '3G')
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# PyPerf will bring it's own argument parser, so configure the script.
|
||||
# The somewhat fast and also precise enough configuration is this. We run
|
||||
# benchmarks 3 times to warmup (e.g especially for download benchmark, this
|
||||
# is important). And then 5 actual runs where we record.
|
||||
sys.argv.extend(
|
||||
['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2']
|
||||
)
|
||||
|
||||
with Context() as context:
|
||||
context.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
287
extras/profiling/run.py
Normal file
287
extras/profiling/run.py
Normal file
@ -0,0 +1,287 @@
|
||||
"""
|
||||
Run the HTTPie benchmark suite with multiple environments.
|
||||
|
||||
This script is configured in a way that, it will create
|
||||
two (or more) isolated environments and compare the *last
|
||||
commit* of this repository with it's master.
|
||||
|
||||
> If you didn't commit yet, it won't be showing results.
|
||||
|
||||
You can also pass --fresh, which would test the *last
|
||||
commit* of this repository with a fresh copy of HTTPie
|
||||
itself. This way even if you don't have an up-to-date
|
||||
master branch, you can still compare it with the upstream's
|
||||
master.
|
||||
|
||||
You can also pass --complex to add 2 additional environments,
|
||||
which would include additional dependencies like pyOpenSSL.
|
||||
|
||||
Examples:
|
||||
|
||||
# Run everything as usual, and compare last commit with master
|
||||
$ python extras/benchmarks/run.py
|
||||
|
||||
# Include complex environments
|
||||
$ python extras/benchmarks/run.py --complex
|
||||
|
||||
# Compare against a fresh copy
|
||||
$ python extras/benchmarks/run.py --fresh
|
||||
|
||||
# Compare against a custom branch of a custom repo
|
||||
$ python extras/benchmarks/run.py --target-repo my_repo --target-branch my_branch
|
||||
|
||||
# Debug changes made on this script (only run benchmarks once)
|
||||
$ python extras/benchmarks/run.py --debug
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import venv
|
||||
from argparse import ArgumentParser, FileType
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import (IO, Dict, Generator, Iterable, List, Optional,
|
||||
Tuple)
|
||||
|
||||
BENCHMARK_SCRIPT = Path(__file__).parent / 'benchmarks.py'
|
||||
CURRENT_REPO = Path(__file__).parent.parent.parent
|
||||
|
||||
GITHUB_URL = 'https://github.com/httpie/httpie.git'
|
||||
TARGET_BRANCH = 'master'
|
||||
|
||||
# Additional dependencies for --complex
|
||||
ADDITIONAL_DEPS = ('pyOpenSSL',)
|
||||
|
||||
|
||||
def call(*args, **kwargs):
|
||||
kwargs.setdefault('stdout', subprocess.DEVNULL)
|
||||
return subprocess.check_call(*args, **kwargs)
|
||||
|
||||
|
||||
class Environment:
|
||||
"""
|
||||
Each environment defines how to create an isolated instance
|
||||
where we could install HTTPie and run benchmarks without any
|
||||
environmental factors.
|
||||
"""
|
||||
|
||||
@contextmanager
|
||||
def on_repo(self) -> Generator[Tuple[Path, Dict[str, str]], None, None]:
|
||||
"""
|
||||
Return the path to the python interpreter and the
|
||||
environment variables (e.g HTTPIE_COMMAND) to be
|
||||
used on the benchmarks.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTTPieEnvironment(Environment):
|
||||
repo_url: str
|
||||
branch: Optional[str] = None
|
||||
dependencies: Iterable[str] = ()
|
||||
|
||||
@contextmanager
|
||||
def on_repo(self) -> Generator[Path, None, None]:
|
||||
with tempfile.TemporaryDirectory() as directory_path:
|
||||
directory = Path(directory_path)
|
||||
|
||||
# Clone the repo
|
||||
repo_path = directory / 'httpie'
|
||||
call(
|
||||
['git', 'clone', self.repo_url, repo_path],
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
if self.branch is not None:
|
||||
call(
|
||||
['git', 'checkout', self.branch],
|
||||
cwd=repo_path,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Prepare the environment
|
||||
venv_path = directory / '.venv'
|
||||
venv.create(venv_path, with_pip=True)
|
||||
|
||||
# Install basic dependencies
|
||||
python = venv_path / 'bin' / 'python'
|
||||
call(
|
||||
[
|
||||
python,
|
||||
'-m',
|
||||
'pip',
|
||||
'install',
|
||||
'wheel',
|
||||
'pyperf==2.3.0',
|
||||
*self.dependencies,
|
||||
]
|
||||
)
|
||||
|
||||
# Create a wheel distribution of HTTPie
|
||||
call([python, 'setup.py', 'bdist_wheel'], cwd=repo_path)
|
||||
|
||||
# Install httpie
|
||||
distribution_path = next((repo_path / 'dist').iterdir())
|
||||
call(
|
||||
[python, '-m', 'pip', 'install', distribution_path],
|
||||
cwd=repo_path,
|
||||
)
|
||||
|
||||
http = venv_path / 'bin' / 'http'
|
||||
yield python, {'HTTPIE_COMMAND': shlex.join([str(python), str(http)])}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalCommandEnvironment(Environment):
|
||||
local_command: str
|
||||
|
||||
@contextmanager
|
||||
def on_repo(self) -> Generator[Path, None, None]:
|
||||
yield sys.executable, {'HTTPIE_COMMAND': self.local_command}
|
||||
|
||||
|
||||
def dump_results(
|
||||
results: List[str],
|
||||
file: IO[str],
|
||||
min_speed: Optional[str] = None
|
||||
) -> None:
|
||||
for result in results:
|
||||
lines = result.strip().splitlines()
|
||||
if min_speed is not None and "hidden" in lines[-1]:
|
||||
lines[-1] = (
|
||||
'Some benchmarks were hidden from this list '
|
||||
'because their timings did not change in a '
|
||||
'significant way (change was within the error '
|
||||
'margin ±{margin}%).'
|
||||
).format(margin=min_speed)
|
||||
result = '\n'.join(lines)
|
||||
|
||||
print(result, file=file)
|
||||
print("\n---\n", file=file)
|
||||
|
||||
|
||||
def compare(*args, directory: Path, min_speed: Optional[str] = None):
|
||||
compare_args = ['pyperf', 'compare_to', '--table', '--table-format=md', *args]
|
||||
if min_speed:
|
||||
compare_args.extend(['--min-speed', min_speed])
|
||||
return subprocess.check_output(
|
||||
compare_args,
|
||||
cwd=directory,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def run(
|
||||
configs: List[Dict[str, Environment]],
|
||||
file: IO[str],
|
||||
debug: bool = False,
|
||||
min_speed: Optional[str] = None,
|
||||
) -> None:
|
||||
result_directory = Path(tempfile.mkdtemp())
|
||||
results = []
|
||||
|
||||
current = 1
|
||||
total = sum(1 for config in configs for _ in config.items())
|
||||
|
||||
def iterate(env_name, status):
|
||||
print(
|
||||
f'Iteration: {env_name} ({current}/{total}) ({status})' + ' ' * 10,
|
||||
end='\r',
|
||||
flush=True,
|
||||
)
|
||||
|
||||
for config in configs:
|
||||
for env_name, env in config.items():
|
||||
iterate(env_name, 'setting up')
|
||||
with env.on_repo() as (python, env_vars):
|
||||
iterate(env_name, 'running benchmarks')
|
||||
args = [python, BENCHMARK_SCRIPT, '-o', env_name]
|
||||
if debug:
|
||||
args.append('--debug-single-value')
|
||||
call(
|
||||
args,
|
||||
cwd=result_directory,
|
||||
env=env_vars,
|
||||
)
|
||||
current += 1
|
||||
|
||||
results.append(compare(
|
||||
*config.keys(),
|
||||
directory=result_directory,
|
||||
min_speed=min_speed
|
||||
))
|
||||
|
||||
dump_results(results, file=file, min_speed=min_speed)
|
||||
print('Results are available at:', result_directory)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--local-repo', default=CURRENT_REPO)
|
||||
parser.add_argument('--local-branch', default=None)
|
||||
parser.add_argument('--target-repo', default=CURRENT_REPO)
|
||||
parser.add_argument('--target-branch', default=TARGET_BRANCH)
|
||||
parser.add_argument(
|
||||
'--fresh',
|
||||
action='store_const',
|
||||
const=GITHUB_URL,
|
||||
dest='target_repo',
|
||||
help='Clone the target repo from upstream GitHub URL',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--complex',
|
||||
action='store_true',
|
||||
help='Add a second run, with a complex python environment.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--local-bin',
|
||||
help='Run the suite with the given local binary in addition to'
|
||||
' existing runners. (E.g --local-bin $(command -v xh))',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
type=FileType('w'),
|
||||
default=sys.stdout,
|
||||
help='File to print the actual results',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-speed',
|
||||
help='Minimum of speed in percent to consider that a '
|
||||
'benchmark is significant'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
configs = []
|
||||
|
||||
base_config = {
|
||||
options.target_branch: HTTPieEnvironment(options.target_repo, options.target_branch),
|
||||
'this_branch': HTTPieEnvironment(options.local_repo, options.local_branch),
|
||||
}
|
||||
configs.append(base_config)
|
||||
|
||||
if options.complex:
|
||||
complex_config = {
|
||||
env_name
|
||||
+ '-complex': dataclasses.replace(env, dependencies=ADDITIONAL_DEPS)
|
||||
for env_name, env in base_config.items()
|
||||
}
|
||||
configs.append(complex_config)
|
||||
|
||||
if options.local_bin:
|
||||
base_config['binary'] = LocalCommandEnvironment(options.local_bin)
|
||||
|
||||
run(configs, file=options.file, debug=options.debug, min_speed=options.min_speed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
HTTPie: command-line HTTP client for the API era.
|
||||
HTTPie: modern, user-friendly command-line HTTP client for the API era.
|
||||
|
||||
"""
|
||||
|
||||
__version__ = '2.5.0'
|
||||
__version__ = '3.0.0'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
13
httpie/adapters.py
Normal file
13
httpie/adapters.py
Normal file
@ -0,0 +1,13 @@
|
||||
from httpie.cli.dicts import HTTPHeadersDict
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
|
||||
class HTTPieHTTPAdapter(HTTPAdapter):
|
||||
|
||||
def build_response(self, req, resp):
|
||||
"""Wrap the original headers with the `HTTPHeadersDict`
|
||||
to preserve multiple headers that have the same name"""
|
||||
|
||||
response = super().build_response(req, resp)
|
||||
response.headers = HTTPHeadersDict(getattr(resp, 'headers', {}))
|
||||
return response
|
@ -15,7 +15,7 @@ from .argtypes import (
|
||||
parse_format_options,
|
||||
)
|
||||
from .constants import (
|
||||
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
HTTP_GET, HTTP_POST, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
|
||||
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
||||
SEPARATOR_CREDENTIALS,
|
||||
@ -50,7 +50,64 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
|
||||
# TODO: refactor and design type-annotated data structures
|
||||
# for raw args + parsed args and keep things immutable.
|
||||
class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
class BaseHTTPieArgumentParser(argparse.ArgumentParser):
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
self.env = None
|
||||
self.args = None
|
||||
self.has_stdin_data = False
|
||||
self.has_input_data = False
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(
|
||||
self,
|
||||
env: Environment,
|
||||
args=None,
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.args, no_options = self.parse_known_args(args, namespace)
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
self.has_stdin_data = (
|
||||
self.env.stdin
|
||||
and not getattr(self.args, 'ignore_stdin', False)
|
||||
and not self.env.stdin_isatty
|
||||
)
|
||||
self.has_input_data = self.has_stdin_data or getattr(self.args, 'raw', None) is not None
|
||||
return self.args
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _print_message(self, message, file=None):
|
||||
# Sneak in our stderr/stdout.
|
||||
if hasattr(self, 'root'):
|
||||
env = self.root.env
|
||||
else:
|
||||
env = self.env
|
||||
|
||||
if env is not None:
|
||||
file = {
|
||||
sys.stdout: env.stdout,
|
||||
sys.stderr: env.stderr,
|
||||
None: env.stderr
|
||||
}.get(file, file)
|
||||
|
||||
if not hasattr(file, 'buffer') and isinstance(message, str):
|
||||
message = message.encode(env.stdout_encoding)
|
||||
super()._print_message(message, file)
|
||||
|
||||
|
||||
class HTTPieManagerArgumentParser(BaseHTTPieArgumentParser):
|
||||
def parse_known_args(self, args=None, namespace=None):
|
||||
try:
|
||||
return super().parse_known_args(args, namespace)
|
||||
except SystemExit as exc:
|
||||
if not hasattr(self, 'root') and exc.code == 2: # Argument Parser Error
|
||||
raise argparse.ArgumentError(None, None)
|
||||
raise
|
||||
|
||||
|
||||
class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||
@ -58,13 +115,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
kwargs['add_help'] = False
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
self.env = None
|
||||
self.args = None
|
||||
self.has_stdin_data = False
|
||||
self.has_input_data = False
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('add_help', False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(
|
||||
@ -120,6 +173,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
}
|
||||
|
||||
def _process_url(self):
|
||||
if self.args.url.startswith('://'):
|
||||
# Paste URL & add space shortcut: `http ://pie.dev` → `http://pie.dev`
|
||||
self.args.url = self.args.url[3:]
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
if os.path.basename(self.env.program_name) == 'https':
|
||||
scheme = 'https://'
|
||||
@ -138,18 +194,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
else:
|
||||
self.args.url = scheme + self.args.url
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _print_message(self, message, file=None):
|
||||
# Sneak in our stderr/stdout.
|
||||
file = {
|
||||
sys.stdout: self.env.stdout,
|
||||
sys.stderr: self.env.stderr,
|
||||
None: self.env.stderr
|
||||
}.get(file, file)
|
||||
if not hasattr(file, 'buffer') and isinstance(message, str):
|
||||
message = message.encode(self.env.stdout_encoding)
|
||||
super()._print_message(message, file)
|
||||
|
||||
def _setup_standard_streams(self):
|
||||
"""
|
||||
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
||||
@ -252,6 +296,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
' --ignore-stdin is set.'
|
||||
)
|
||||
credentials.prompt_password(url.netloc)
|
||||
|
||||
if (credentials.key and credentials.value):
|
||||
plugin.raw_auth = credentials.key + ":" + credentials.value
|
||||
|
||||
self.args.auth = plugin.get_auth(
|
||||
username=credentials.key,
|
||||
password=credentials.value,
|
||||
@ -311,7 +359,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self.error('Request body (from stdin, --raw or a file) and request '
|
||||
'data (key=value) cannot be mixed. Pass '
|
||||
'--ignore-stdin to let key/value take priority. '
|
||||
'See https://httpie.org/doc#scripting for details.')
|
||||
'See https://httpie.io/docs#scripting for details.')
|
||||
|
||||
def _guess_method(self):
|
||||
"""Set `args.method` if not specified to either POST or GET
|
||||
@ -361,7 +409,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
try:
|
||||
request_items = RequestItems.from_args(
|
||||
request_item_args=self.args.request_items,
|
||||
as_form=self.args.form,
|
||||
request_type=self.args.request_type,
|
||||
)
|
||||
except ParseError as e:
|
||||
if self.args.traceback:
|
||||
@ -412,8 +460,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self.args.all = True
|
||||
|
||||
if self.args.output_options is None:
|
||||
if self.args.verbose:
|
||||
if self.args.verbose >= 2:
|
||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||
elif self.args.verbose == 1:
|
||||
self.args.output_options = ''.join(BASE_OUTPUT_OPTIONS)
|
||||
elif self.args.offline:
|
||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
||||
elif not self.env.stdout_isatty:
|
||||
@ -457,7 +507,8 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
self.error('--continue requires --output to be specified')
|
||||
|
||||
def _process_format_options(self):
|
||||
format_options = self.args.format_options or []
|
||||
parsed_options = PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
for options_group in self.args.format_options or []:
|
||||
for options_group in format_options:
|
||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||
self.args.format_options = parsed_options
|
||||
|
@ -57,12 +57,12 @@ class KeyValueArgType:
|
||||
|
||||
def __init__(self, *separators: str):
|
||||
self.separators = separators
|
||||
self.special_characters = set('\\')
|
||||
self.special_characters = set()
|
||||
for separator in separators:
|
||||
self.special_characters.update(separator)
|
||||
|
||||
def __call__(self, s: str) -> KeyValueArg:
|
||||
"""Parse raw string arg and return `self.key_value_class` instance.
|
||||
"""Parse raw string arg and return `self.key_value_class` instance.
|
||||
|
||||
The best of `self.separators` is determined (first found, longest).
|
||||
Back slash escaped characters aren't considered as separators
|
||||
@ -113,7 +113,7 @@ class KeyValueArgType:
|
||||
There are only two token types - strings and escaped characters:
|
||||
|
||||
>>> KeyValueArgType('=').tokenize(r'foo\=bar\\baz')
|
||||
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
|
||||
['foo', Escaped('='), 'bar\\\\baz']
|
||||
|
||||
"""
|
||||
tokens = ['']
|
||||
@ -242,3 +242,19 @@ PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
|
||||
s=','.join(DEFAULT_FORMAT_OPTIONS),
|
||||
defaults=None,
|
||||
)
|
||||
|
||||
|
||||
def response_charset_type(encoding: str) -> str:
|
||||
try:
|
||||
''.encode(encoding)
|
||||
except LookupError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'{encoding!r} is not a supported encoding')
|
||||
return encoding
|
||||
|
||||
|
||||
def response_mime_type(mime_type: str) -> str:
|
||||
if mime_type.count('/') != 1:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'{mime_type!r} doesn’t look like a mime type; use type/subtype')
|
||||
return mime_type
|
||||
|
@ -15,6 +15,7 @@ SEPARATOR_HEADER = ':'
|
||||
SEPARATOR_HEADER_EMPTY = ';'
|
||||
SEPARATOR_CREDENTIALS = ':'
|
||||
SEPARATOR_PROXY = ':'
|
||||
SEPARATOR_HEADER_EMBED = ':@'
|
||||
SEPARATOR_DATA_STRING = '='
|
||||
SEPARATOR_DATA_RAW_JSON = ':='
|
||||
SEPARATOR_FILE_UPLOAD = '@'
|
||||
@ -22,6 +23,7 @@ SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEPARATOR_QUERY_PARAM = '=='
|
||||
SEPARATOR_QUERY_EMBED_FILE = '==@'
|
||||
|
||||
# Separators that become request data
|
||||
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
|
||||
@ -40,13 +42,17 @@ SEPARATORS_GROUP_MULTIPART = frozenset({
|
||||
|
||||
# Separators for items whose value is a filename to be embedded
|
||||
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
|
||||
SEPARATOR_HEADER_EMBED,
|
||||
SEPARATOR_QUERY_EMBED_FILE,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
})
|
||||
|
||||
# Separators for raw JSON items
|
||||
SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
# Separators for nested JSON items
|
||||
SEPARATOR_GROUP_NESTED_JSON_ITEMS = frozenset([
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
@ -54,7 +60,9 @@ SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
|
||||
SEPARATOR_HEADER,
|
||||
SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_HEADER_EMBED,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_QUERY_EMBED_FILE,
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
@ -67,12 +75,18 @@ OUT_REQ_HEAD = 'H'
|
||||
OUT_REQ_BODY = 'B'
|
||||
OUT_RESP_HEAD = 'h'
|
||||
OUT_RESP_BODY = 'b'
|
||||
OUT_RESP_META = 'm'
|
||||
|
||||
OUTPUT_OPTIONS = frozenset({
|
||||
BASE_OUTPUT_OPTIONS = frozenset({
|
||||
OUT_REQ_HEAD,
|
||||
OUT_REQ_BODY,
|
||||
OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY
|
||||
OUT_RESP_BODY,
|
||||
})
|
||||
|
||||
OUTPUT_OPTIONS = frozenset({
|
||||
*BASE_OUTPUT_OPTIONS,
|
||||
OUT_RESP_META,
|
||||
})
|
||||
|
||||
# Pretty
|
||||
@ -111,3 +125,9 @@ class RequestType(enum.Enum):
|
||||
FORM = enum.auto()
|
||||
MULTIPART = enum.auto()
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
OPEN_BRACKET = '['
|
||||
CLOSE_BRACKET = ']'
|
||||
BACKSLASH = '\\'
|
||||
HIGHLIGHTER = '^'
|
||||
|
@ -9,28 +9,29 @@ from .. import __doc__, __version__
|
||||
from .argparser import HTTPieArgumentParser
|
||||
from .argtypes import (
|
||||
KeyValueArgType, SessionNameValidator,
|
||||
readable_file_arg,
|
||||
readable_file_arg, response_charset_type, response_mime_type,
|
||||
)
|
||||
from .constants import (
|
||||
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
||||
DEFAULT_FORMAT_OPTIONS, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, OUT_RESP_META, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||
SORTED_FORMAT_OPTIONS_STRING,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING,
|
||||
)
|
||||
from .utils import LazyChoices
|
||||
from ..output.formatters.colors import (
|
||||
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
|
||||
AUTO_STYLE, DEFAULT_STYLE, get_available_styles
|
||||
)
|
||||
from ..plugins.builtin import BuiltinAuthPlugin
|
||||
from ..plugins.registry import plugin_manager
|
||||
from ..sessions import DEFAULT_SESSIONS_DIR
|
||||
from ..ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
from ..ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
prog='http',
|
||||
description=f'{__doc__.strip()} <https://httpie.org>',
|
||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||
epilog=dedent('''
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
@ -41,6 +42,7 @@ parser = HTTPieArgumentParser(
|
||||
|
||||
'''),
|
||||
)
|
||||
parser.register('action', 'lazy_choices', LazyChoices)
|
||||
|
||||
#######################################################################
|
||||
# Positional arguments.
|
||||
@ -96,7 +98,7 @@ positional.add_argument(
|
||||
|
||||
':' HTTP headers:
|
||||
|
||||
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
'==' URL parameters to be appended to the request URI:
|
||||
|
||||
@ -118,7 +120,7 @@ positional.add_argument(
|
||||
|
||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||
|
||||
essay=@Documents/essay.txt
|
||||
essay=@Documents/essay.txt
|
||||
|
||||
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
|
||||
|
||||
@ -247,32 +249,38 @@ output_processing.add_argument(
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
def format_style_help(available_styles):
|
||||
return '''
|
||||
Output coloring style (default is "{default}"). It can be one of:
|
||||
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
'''.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in wrap(', '.join(available_styles), 60)
|
||||
).strip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
|
||||
|
||||
output_processing.add_argument(
|
||||
'--style', '-s',
|
||||
dest='style',
|
||||
metavar='STYLE',
|
||||
default=DEFAULT_STYLE,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help='''
|
||||
Output coloring style (default is "{default}"). It can be One of:
|
||||
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
|
||||
'''.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).strip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
action='lazy_choices',
|
||||
getter=get_available_styles,
|
||||
help_formatter=format_style_help
|
||||
)
|
||||
|
||||
_sorted_kwargs = {
|
||||
'action': 'append_const',
|
||||
'const': SORTED_FORMAT_OPTIONS_STRING,
|
||||
@ -309,6 +317,31 @@ output_processing.add_argument(
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--response-charset',
|
||||
metavar='ENCODING',
|
||||
type=response_charset_type,
|
||||
help='''
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
--response-charset=utf8
|
||||
--response-charset=big5
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--response-mime',
|
||||
metavar='MIME_TYPE',
|
||||
type=response_mime_type,
|
||||
help='''
|
||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||
|
||||
--response-mime=application/json
|
||||
--response-mime=text/xml
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--format-options',
|
||||
@ -350,6 +383,7 @@ output_options.add_argument(
|
||||
'{OUT_REQ_BODY}' request body
|
||||
'{OUT_RESP_HEAD}' response headers
|
||||
'{OUT_RESP_BODY}' response body
|
||||
'{OUT_RESP_META}' response metadata
|
||||
|
||||
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
@ -368,6 +402,16 @@ output_options.add_argument(
|
||||
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--meta', '-m',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_META,
|
||||
help=f'''
|
||||
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
||||
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b',
|
||||
dest='output_options',
|
||||
@ -382,12 +426,16 @@ output_options.add_argument(
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v',
|
||||
dest='verbose',
|
||||
action='store_true',
|
||||
action='count',
|
||||
default=0,
|
||||
help=f'''
|
||||
Verbose output. Print the whole request as well as the response. Also print
|
||||
any intermediary requests/responses (such as redirects).
|
||||
It's a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
||||
Verbose output. For the level one (with single `-v`/`--verbose`), 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: --all --print={''.join(BASE_OUTPUT_OPTIONS)}
|
||||
Level two is a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
@ -472,12 +520,14 @@ output_options.add_argument(
|
||||
|
||||
output_options.add_argument(
|
||||
'--quiet', '-q',
|
||||
action='store_true',
|
||||
default=False,
|
||||
action='count',
|
||||
default=0,
|
||||
help='''
|
||||
Do not print to stdout or stderr.
|
||||
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 --output is specified.
|
||||
Flag doesn't affect behaviour of download beyond not printing to terminal.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
@ -527,36 +577,24 @@ auth = parser.add_argument_group(title='Authentication')
|
||||
auth.add_argument(
|
||||
'--auth', '-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS]',
|
||||
metavar='USER[:PASS] | TOKEN',
|
||||
help='''
|
||||
If only the username is provided (-a username), HTTPie will prompt
|
||||
for the password.
|
||||
For username/password based authentication mechanisms (e.g
|
||||
basic auth or digest auth) if only the username is provided
|
||||
(-a username), HTTPie will prompt for the password.
|
||||
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
class _AuthTypeLazyChoices:
|
||||
# Needed for plugin testing
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in plugin_manager.get_auth_plugin_mapping()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys()))
|
||||
|
||||
|
||||
_auth_plugins = plugin_manager.get_auth_plugins()
|
||||
auth.add_argument(
|
||||
'--auth-type', '-A',
|
||||
choices=_AuthTypeLazyChoices(),
|
||||
default=None,
|
||||
help='''
|
||||
def format_auth_help(auth_plugins_mapping):
|
||||
auth_plugins = list(auth_plugins_mapping.values())
|
||||
return '''
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
|
||||
'''.format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'''.format(default=auth_plugins[0].auth_type, types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
@ -569,8 +607,18 @@ auth.add_argument(
|
||||
'\n ' + ('\n '.join(wrap(plugin.description)))
|
||||
)
|
||||
)
|
||||
for plugin in _auth_plugins
|
||||
)),
|
||||
for plugin in auth_plugins
|
||||
))
|
||||
|
||||
|
||||
auth.add_argument(
|
||||
'--auth-type', '-A',
|
||||
action='lazy_choices',
|
||||
default=None,
|
||||
getter=plugin_manager.get_auth_plugin_mapping,
|
||||
sort=True,
|
||||
cache=False,
|
||||
help_formatter=format_auth_help,
|
||||
)
|
||||
auth.add_argument(
|
||||
'--ignore-netrc',
|
||||
@ -686,9 +734,11 @@ network.add_argument(
|
||||
'--chunked',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Enable streaming via chunked transfer encoding.
|
||||
The Transfer-Encoding header is set to chunked.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
|
@ -1,15 +1,41 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from multidict import MultiDict, CIMultiDict
|
||||
|
||||
|
||||
class RequestHeadersDict(CaseInsensitiveDict):
|
||||
class BaseMultiDict(MultiDict):
|
||||
"""
|
||||
Headers are case-insensitive and multiple values are currently not supported.
|
||||
|
||||
Base class for all MultiDicts.
|
||||
"""
|
||||
|
||||
|
||||
class HTTPHeadersDict(CIMultiDict, BaseMultiDict):
|
||||
"""
|
||||
Headers are case-insensitive and multiple values are supported
|
||||
through the `add()` API.
|
||||
"""
|
||||
|
||||
def add(self, key, value):
|
||||
"""
|
||||
Add or update a new header.
|
||||
|
||||
If the given `value` is `None`, then all the previous
|
||||
values will be overwritten and the value will be set
|
||||
to `None`.
|
||||
"""
|
||||
if value is None:
|
||||
self[key] = value
|
||||
return None
|
||||
|
||||
# If the previous value for the given header is `None`
|
||||
# then discard it since we are explicitly giving a new
|
||||
# value for it.
|
||||
if key in self and self.getone(key) is None:
|
||||
self.popone(key)
|
||||
|
||||
super().add(key, value)
|
||||
|
||||
|
||||
class RequestJSONDataDict(OrderedDict):
|
||||
pass
|
||||
|
||||
|
344
httpie/cli/nested_json.py
Normal file
344
httpie/cli/nested_json.py
Normal file
@ -0,0 +1,344 @@
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Any,
|
||||
Iterator,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
List,
|
||||
NoReturn,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from httpie.cli.constants import OPEN_BRACKET, CLOSE_BRACKET, BACKSLASH, HIGHLIGHTER
|
||||
|
||||
|
||||
class HTTPieSyntaxError(ValueError):
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
token: Optional['Token'],
|
||||
message: str,
|
||||
message_kind: str = 'Syntax',
|
||||
) -> None:
|
||||
self.source = source
|
||||
self.token = token
|
||||
self.message = message
|
||||
self.message_kind = message_kind
|
||||
|
||||
def __str__(self):
|
||||
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
|
||||
if self.token is not None:
|
||||
lines.append(self.source)
|
||||
lines.append(
|
||||
' ' * (self.token.start)
|
||||
+ HIGHLIGHTER * (self.token.end - self.token.start)
|
||||
)
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class TokenKind(Enum):
|
||||
TEXT = auto()
|
||||
NUMBER = auto()
|
||||
LEFT_BRACKET = auto()
|
||||
RIGHT_BRACKET = auto()
|
||||
|
||||
def to_name(self) -> str:
|
||||
for key, value in OPERATORS.items():
|
||||
if value is self:
|
||||
return repr(key)
|
||||
else:
|
||||
return 'a ' + self.name.lower()
|
||||
|
||||
|
||||
OPERATORS = {OPEN_BRACKET: TokenKind.LEFT_BRACKET, CLOSE_BRACKET: TokenKind.RIGHT_BRACKET}
|
||||
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
|
||||
|
||||
|
||||
class Token(NamedTuple):
|
||||
kind: TokenKind
|
||||
value: Union[str, int]
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
def assert_cant_happen() -> NoReturn:
|
||||
raise ValueError('Unexpected value')
|
||||
|
||||
|
||||
def check_escaped_int(value: str) -> str:
|
||||
if not value.startswith(BACKSLASH):
|
||||
raise ValueError('Not an escaped int')
|
||||
|
||||
try:
|
||||
int(value[1:])
|
||||
except ValueError as exc:
|
||||
raise ValueError('Not an escaped int') from exc
|
||||
else:
|
||||
return value[1:]
|
||||
|
||||
|
||||
def tokenize(source: str) -> Iterator[Token]:
|
||||
cursor = 0
|
||||
backslashes = 0
|
||||
buffer = []
|
||||
|
||||
def send_buffer() -> Iterator[Token]:
|
||||
nonlocal backslashes
|
||||
if not buffer:
|
||||
return None
|
||||
|
||||
value = ''.join(buffer)
|
||||
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
|
||||
|
||||
yield Token(
|
||||
kind, value, start=cursor - (len(buffer) + backslashes), end=cursor
|
||||
)
|
||||
buffer.clear()
|
||||
backslashes = 0
|
||||
|
||||
def can_advance() -> bool:
|
||||
return cursor < len(source)
|
||||
|
||||
while can_advance():
|
||||
index = source[cursor]
|
||||
if index in OPERATORS:
|
||||
yield from send_buffer()
|
||||
yield Token(OPERATORS[index], index, cursor, cursor + 1)
|
||||
elif index == BACKSLASH and can_advance():
|
||||
if source[cursor + 1] in SPECIAL_CHARS:
|
||||
backslashes += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
|
||||
buffer.append(source[cursor + 1])
|
||||
cursor += 1
|
||||
else:
|
||||
buffer.append(index)
|
||||
|
||||
cursor += 1
|
||||
|
||||
yield from send_buffer()
|
||||
|
||||
|
||||
class PathAction(Enum):
|
||||
KEY = auto()
|
||||
INDEX = auto()
|
||||
APPEND = auto()
|
||||
|
||||
# Pseudo action, used by the interpreter
|
||||
SET = auto()
|
||||
|
||||
def to_string(self) -> str:
|
||||
return self.name.lower()
|
||||
|
||||
|
||||
class Path:
|
||||
def __init__(
|
||||
self,
|
||||
kind: PathAction,
|
||||
accessor: Optional[Union[str, int]] = None,
|
||||
tokens: Optional[List[Token]] = None,
|
||||
is_root: bool = False,
|
||||
):
|
||||
self.kind = kind
|
||||
self.accessor = accessor
|
||||
self.tokens = tokens or []
|
||||
self.is_root = is_root
|
||||
|
||||
def reconstruct(self) -> str:
|
||||
if self.kind is PathAction.KEY:
|
||||
if self.is_root:
|
||||
return str(self.accessor)
|
||||
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.INDEX:
|
||||
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
|
||||
elif self.kind is PathAction.APPEND:
|
||||
return OPEN_BRACKET + CLOSE_BRACKET
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
|
||||
def parse(source: str) -> Iterator[Path]:
|
||||
"""
|
||||
start: literal? path*
|
||||
|
||||
literal: TEXT | NUMBER
|
||||
|
||||
path:
|
||||
key_path
|
||||
| index_path
|
||||
| append_path
|
||||
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
|
||||
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
|
||||
append_path: LEFT_BRACKET RIGHT_BRACKET
|
||||
"""
|
||||
|
||||
tokens = list(tokenize(source))
|
||||
cursor = 0
|
||||
|
||||
def can_advance():
|
||||
return cursor < len(tokens)
|
||||
|
||||
def expect(*kinds):
|
||||
nonlocal cursor
|
||||
|
||||
assert len(kinds) > 0
|
||||
if can_advance():
|
||||
token = tokens[cursor]
|
||||
cursor += 1
|
||||
if token.kind in kinds:
|
||||
return token
|
||||
elif tokens:
|
||||
token = tokens[-1]._replace(
|
||||
start=tokens[-1].end + 0, end=tokens[-1].end + 1
|
||||
)
|
||||
else:
|
||||
token = None
|
||||
|
||||
if len(kinds) == 1:
|
||||
suffix = kinds[0].to_name()
|
||||
else:
|
||||
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
|
||||
suffix += ' or ' + kinds[-1].to_name()
|
||||
|
||||
message = f'Expecting {suffix}'
|
||||
raise HTTPieSyntaxError(source, token, message)
|
||||
|
||||
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)
|
||||
|
||||
yield root
|
||||
|
||||
while can_advance():
|
||||
path_tokens = []
|
||||
path_tokens.append(expect(TokenKind.LEFT_BRACKET))
|
||||
|
||||
token = expect(
|
||||
TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET
|
||||
)
|
||||
path_tokens.append(token)
|
||||
if token.kind is TokenKind.RIGHT_BRACKET:
|
||||
path = Path(PathAction.APPEND, tokens=path_tokens)
|
||||
elif token.kind is TokenKind.TEXT:
|
||||
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
elif token.kind is TokenKind.NUMBER:
|
||||
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
|
||||
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
|
||||
else:
|
||||
assert_cant_happen()
|
||||
yield path
|
||||
|
||||
|
||||
JSON_TYPE_MAPPING = {
|
||||
dict: 'object',
|
||||
list: 'array',
|
||||
int: 'number',
|
||||
float: 'number',
|
||||
str: 'string',
|
||||
}
|
||||
|
||||
|
||||
def interpret(context: Any, key: str, value: Any) -> Any:
|
||||
cursor = context
|
||||
|
||||
paths = list(parse(key))
|
||||
paths.append(Path(PathAction.SET, value))
|
||||
|
||||
def type_check(index: int, path: Path, expected_type: Type[Any]) -> None:
|
||||
if not isinstance(cursor, expected_type):
|
||||
if path.tokens:
|
||||
pseudo_token = Token(
|
||||
None, None, path.tokens[0].start, path.tokens[-1].end
|
||||
)
|
||||
else:
|
||||
pseudo_token = None
|
||||
|
||||
cursor_type = JSON_TYPE_MAPPING.get(
|
||||
type(cursor), type(cursor).__name__
|
||||
)
|
||||
required_type = JSON_TYPE_MAPPING[expected_type]
|
||||
|
||||
message = f"Can't perform {path.kind.to_string()!r} based access on "
|
||||
message += repr(
|
||||
''.join(path.reconstruct() for path in paths[:index])
|
||||
)
|
||||
message += (
|
||||
f' which has a type of {cursor_type!r} but this operation'
|
||||
)
|
||||
message += f' requires a type of {required_type!r}.'
|
||||
raise HTTPieSyntaxError(
|
||||
key, pseudo_token, message, message_kind='Type'
|
||||
)
|
||||
|
||||
def object_for(kind: str) -> Any:
|
||||
if kind is PathAction.KEY:
|
||||
return {}
|
||||
elif kind in {PathAction.INDEX, PathAction.APPEND}:
|
||||
return []
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
|
||||
if path.kind is PathAction.KEY:
|
||||
type_check(index, path, dict)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
|
||||
cursor = cursor.setdefault(
|
||||
path.accessor, object_for(next_path.kind)
|
||||
)
|
||||
elif path.kind is PathAction.INDEX:
|
||||
type_check(index, path, list)
|
||||
if path.accessor < 0:
|
||||
raise HTTPieSyntaxError(
|
||||
key,
|
||||
path.tokens[1],
|
||||
'Negative indexes are not supported.',
|
||||
message_kind='Value',
|
||||
)
|
||||
cursor.extend([None] * (path.accessor - len(cursor) + 1))
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor[path.accessor] = next_path.accessor
|
||||
break
|
||||
|
||||
if cursor[path.accessor] is None:
|
||||
cursor[path.accessor] = object_for(next_path.kind)
|
||||
|
||||
cursor = cursor[path.accessor]
|
||||
elif path.kind is PathAction.APPEND:
|
||||
type_check(index, path, list)
|
||||
if next_path.kind is PathAction.SET:
|
||||
cursor.append(next_path.accessor)
|
||||
break
|
||||
|
||||
cursor.append(object_for(next_path.kind))
|
||||
cursor = cursor[-1]
|
||||
else:
|
||||
assert_cant_happen()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
def interpret_nested_json(pairs):
|
||||
context = {}
|
||||
for key, value in pairs:
|
||||
interpret(context, key, value)
|
||||
return context
|
@ -1,28 +1,33 @@
|
||||
import os
|
||||
import functools
|
||||
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
|
||||
|
||||
from .argtypes import KeyValueArg
|
||||
from .constants import (
|
||||
SEPARATORS_GROUP_MULTIPART, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE, SEPARATOR_GROUP_NESTED_JSON_ITEMS,
|
||||
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_HEADER_EMBED, SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_QUERY_EMBED_FILE, RequestType
|
||||
)
|
||||
from .dicts import (
|
||||
MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
|
||||
RequestHeadersDict, RequestJSONDataDict,
|
||||
BaseMultiDict, MultipartRequestDataDict, RequestDataDict,
|
||||
RequestFilesDict, HTTPHeadersDict, RequestJSONDataDict,
|
||||
RequestQueryParamsDict,
|
||||
)
|
||||
from .exceptions import ParseError
|
||||
from ..utils import get_content_type, load_json_preserve_order
|
||||
from .nested_json import interpret_nested_json
|
||||
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split
|
||||
|
||||
|
||||
class RequestItems:
|
||||
|
||||
def __init__(self, as_form=False):
|
||||
self.headers = RequestHeadersDict()
|
||||
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
|
||||
def __init__(self, request_type: Optional[RequestType] = None):
|
||||
self.headers = HTTPHeadersDict()
|
||||
self.request_type = request_type
|
||||
self.is_json = request_type is None or request_type is RequestType.JSON
|
||||
self.data = RequestJSONDataDict() if self.is_json else RequestDataDict()
|
||||
self.files = RequestFilesDict()
|
||||
self.params = RequestQueryParamsDict()
|
||||
# To preserve the order of fields in file upload multipart requests.
|
||||
@ -32,9 +37,9 @@ class RequestItems:
|
||||
def from_args(
|
||||
cls,
|
||||
request_item_args: List[KeyValueArg],
|
||||
as_form=False,
|
||||
request_type: Optional[RequestType] = None,
|
||||
) -> 'RequestItems':
|
||||
instance = cls(as_form=as_form)
|
||||
instance = cls(request_type=request_type)
|
||||
rules: Dict[str, Tuple[Callable, dict]] = {
|
||||
SEPARATOR_HEADER: (
|
||||
process_header_arg,
|
||||
@ -44,10 +49,18 @@ class RequestItems:
|
||||
process_empty_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_HEADER_EMBED: (
|
||||
process_embed_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_QUERY_PARAM: (
|
||||
process_query_param_arg,
|
||||
instance.params,
|
||||
),
|
||||
SEPARATOR_QUERY_EMBED_FILE: (
|
||||
process_embed_query_param_arg,
|
||||
instance.params,
|
||||
),
|
||||
SEPARATOR_FILE_UPLOAD: (
|
||||
process_file_upload_arg,
|
||||
instance.files,
|
||||
@ -60,24 +73,47 @@ class RequestItems:
|
||||
process_data_embed_file_contents_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_GROUP_NESTED_JSON_ITEMS: (
|
||||
process_data_nested_json_embed_args,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_RAW_JSON: (
|
||||
process_data_raw_json_embed_arg,
|
||||
json_only(instance, process_data_raw_json_embed_arg),
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
|
||||
process_data_embed_raw_json_file_arg,
|
||||
json_only(instance, process_data_embed_raw_json_file_arg),
|
||||
instance.data,
|
||||
),
|
||||
}
|
||||
|
||||
if instance.is_json:
|
||||
json_item_args, request_item_args = split(
|
||||
request_item_args,
|
||||
lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
|
||||
)
|
||||
if json_item_args:
|
||||
pairs = [
|
||||
(arg.key, rules[arg.sep][0](arg))
|
||||
for arg in json_item_args
|
||||
]
|
||||
processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS]
|
||||
value = processor_func(pairs)
|
||||
target_dict.update(value)
|
||||
|
||||
# Then handle all other items.
|
||||
for arg in request_item_args:
|
||||
processor_func, target_dict = rules[arg.sep]
|
||||
value = processor_func(arg)
|
||||
target_dict[arg.key] = value
|
||||
|
||||
if arg.sep in SEPARATORS_GROUP_MULTIPART:
|
||||
instance.multipart_data[arg.key] = value
|
||||
|
||||
if isinstance(target_dict, BaseMultiDict):
|
||||
target_dict.add(arg.key, value)
|
||||
else:
|
||||
target_dict[arg.key] = value
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
@ -88,6 +124,10 @@ def process_header_arg(arg: KeyValueArg) -> Optional[str]:
|
||||
return arg.value or None
|
||||
|
||||
|
||||
def process_embed_header_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg).rstrip('\n')
|
||||
|
||||
|
||||
def process_empty_header_arg(arg: KeyValueArg) -> str:
|
||||
if not arg.value:
|
||||
return arg.value
|
||||
@ -100,6 +140,10 @@ def process_query_param_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_embed_query_param_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg).rstrip('\n')
|
||||
|
||||
|
||||
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
||||
filename = parts[0]
|
||||
@ -123,6 +167,29 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg)
|
||||
|
||||
|
||||
def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
|
||||
if items.is_json:
|
||||
return func
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs) -> str:
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
except ParseError:
|
||||
ret = None
|
||||
|
||||
# If it is a basic type, then allow it
|
||||
if isinstance(ret, (str, int, float)):
|
||||
return str(ret)
|
||||
else:
|
||||
raise ParseError(
|
||||
'Can\'t use complex JSON value types with '
|
||||
'--form/--multipart.'
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
|
||||
contents = load_text_file(arg)
|
||||
value = load_json(arg, contents)
|
||||
@ -134,6 +201,10 @@ def process_data_raw_json_embed_arg(arg: KeyValueArg) -> JSONType:
|
||||
return value
|
||||
|
||||
|
||||
def process_data_nested_json_embed_args(pairs) -> Dict[str, JSONType]:
|
||||
return interpret_nested_json(pairs)
|
||||
|
||||
|
||||
def load_text_file(item: KeyValueArg) -> str:
|
||||
path = item.value
|
||||
try:
|
||||
@ -150,6 +221,6 @@ def load_text_file(item: KeyValueArg) -> str:
|
||||
|
||||
def load_json(arg: KeyValueArg, contents: str) -> JSONType:
|
||||
try:
|
||||
return load_json_preserve_order(contents)
|
||||
return load_json_preserve_order_and_dupe_keys(contents)
|
||||
except ValueError as e:
|
||||
raise ParseError(f'{arg.orig!r}: {e}')
|
||||
|
53
httpie/cli/utils.py
Normal file
53
httpie/cli/utils.py
Normal file
@ -0,0 +1,53 @@
|
||||
import argparse
|
||||
from typing import Any, Callable, Generic, Iterator, Iterable, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class LazyChoices(argparse.Action, Generic[T]):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
getter: Callable[[], Iterable[T]],
|
||||
help_formatter: Optional[Callable[[T], str]] = None,
|
||||
sort: bool = False,
|
||||
cache: bool = True,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.getter = getter
|
||||
self.help_formatter = help_formatter
|
||||
self.sort = sort
|
||||
self.cache = cache
|
||||
self._help: Optional[str] = None
|
||||
self._obj: Optional[Iterable[T]] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
self.choices = self
|
||||
|
||||
def load(self) -> T:
|
||||
if self._obj is None or not self.cache:
|
||||
self._obj = self.getter()
|
||||
|
||||
assert self._obj is not None
|
||||
return self._obj
|
||||
|
||||
@property
|
||||
def help(self) -> str:
|
||||
if self._help is None and self.help_formatter is not None:
|
||||
self._help = self.help_formatter(self.load())
|
||||
return self._help
|
||||
|
||||
@help.setter
|
||||
def help(self, value: Any) -> None:
|
||||
self._help = value
|
||||
|
||||
def __contains__(self, item: Any) -> bool:
|
||||
return item in self.load()
|
||||
|
||||
def __iter__(self) -> Iterator[T]:
|
||||
if self.sort:
|
||||
return iter(sorted(self.load()))
|
||||
else:
|
||||
return iter(self.load())
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setattr(namespace, self.dest, values)
|
@ -3,19 +3,21 @@ import http.client
|
||||
import json
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, Union
|
||||
from typing import Any, Dict, Callable, Iterable
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
from . import __version__
|
||||
from .cli.dicts import RequestHeadersDict
|
||||
from .constants import UTF8
|
||||
from .adapters import HTTPieHTTPAdapter
|
||||
from .context import Environment
|
||||
from .cli.dicts import HTTPHeadersDict
|
||||
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, HTTPieHTTPSAdapter
|
||||
from .uploads import (
|
||||
compress_request, prepare_request_body,
|
||||
get_multipart_data_and_content_type,
|
||||
@ -32,15 +34,15 @@ DEFAULT_UA = f'HTTPie/{__version__}'
|
||||
|
||||
|
||||
def collect_messages(
|
||||
env: Environment,
|
||||
args: argparse.Namespace,
|
||||
config_dir: Path,
|
||||
request_body_read_callback: Callable[[bytes], None] = None,
|
||||
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
|
||||
) -> Iterable[RequestsMessage]:
|
||||
httpie_session = None
|
||||
httpie_session_headers = None
|
||||
if args.session or args.session_read_only:
|
||||
httpie_session = get_httpie_session(
|
||||
config_dir=config_dir,
|
||||
config_dir=env.config.directory,
|
||||
session_name=args.session or args.session_read_only,
|
||||
host=args.headers.get('Host'),
|
||||
url=args.url,
|
||||
@ -48,6 +50,7 @@ def collect_messages(
|
||||
httpie_session_headers = httpie_session.headers
|
||||
|
||||
request_kwargs = make_request_kwargs(
|
||||
env,
|
||||
args=args,
|
||||
base_headers=httpie_session_headers,
|
||||
request_body_read_callback=request_body_read_callback
|
||||
@ -79,6 +82,7 @@ def collect_messages(
|
||||
|
||||
request = requests.Request(**request_kwargs)
|
||||
prepared_request = requests_session.prepare_request(request)
|
||||
apply_missing_repeated_headers(prepared_request, request.headers)
|
||||
if args.path_as_is:
|
||||
prepared_request.url = ensure_path_as_is(
|
||||
orig_url=args.url,
|
||||
@ -152,6 +156,7 @@ def build_requests_session(
|
||||
requests_session = requests.Session()
|
||||
|
||||
# Install our adapter.
|
||||
http_adapter = HTTPieHTTPAdapter()
|
||||
https_adapter = HTTPieHTTPSAdapter(
|
||||
ciphers=ciphers,
|
||||
verify=verify,
|
||||
@ -160,6 +165,7 @@ def build_requests_session(
|
||||
if ssl_version else None
|
||||
),
|
||||
)
|
||||
requests_session.mount('http://', http_adapter)
|
||||
requests_session.mount('https://', https_adapter)
|
||||
|
||||
# Install adapters from plugins.
|
||||
@ -178,8 +184,8 @@ def dump_request(kwargs: dict):
|
||||
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
|
||||
|
||||
|
||||
def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
|
||||
final_headers = RequestHeadersDict()
|
||||
def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
|
||||
final_headers = HTTPHeadersDict()
|
||||
for name, value in headers.items():
|
||||
if value is not None:
|
||||
# “leading or trailing LWS MAY be removed without
|
||||
@ -190,12 +196,42 @@ def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
|
||||
if isinstance(value, str):
|
||||
# See <https://github.com/httpie/httpie/issues/212>
|
||||
value = value.encode()
|
||||
final_headers[name] = value
|
||||
final_headers.add(name, value)
|
||||
return final_headers
|
||||
|
||||
|
||||
def make_default_headers(args: argparse.Namespace) -> RequestHeadersDict:
|
||||
default_headers = RequestHeadersDict({
|
||||
def apply_missing_repeated_headers(
|
||||
prepared_request: requests.PreparedRequest,
|
||||
original_headers: HTTPHeadersDict
|
||||
) -> None:
|
||||
"""Update the given `prepared_request`'s headers with the original
|
||||
ones. This allows the requests to be prepared as usual, and then later
|
||||
merged with headers that are specified multiple times."""
|
||||
|
||||
new_headers = HTTPHeadersDict(prepared_request.headers)
|
||||
for prepared_name, prepared_value in prepared_request.headers.items():
|
||||
if prepared_name not in original_headers:
|
||||
continue
|
||||
|
||||
original_keys, original_values = zip(*filter(
|
||||
lambda item: item[0].casefold() == prepared_name.casefold(),
|
||||
original_headers.items()
|
||||
))
|
||||
|
||||
if prepared_value not in original_values:
|
||||
# If the current value is not among the initial values
|
||||
# set for this field, then it means that this field got
|
||||
# overridden on the way, and we should preserve it.
|
||||
continue
|
||||
|
||||
new_headers.popone(prepared_name)
|
||||
new_headers.update(zip(original_keys, original_values))
|
||||
|
||||
prepared_request.headers = new_headers
|
||||
|
||||
|
||||
def make_default_headers(args: argparse.Namespace) -> HTTPHeadersDict:
|
||||
default_headers = HTTPHeadersDict({
|
||||
'User-Agent': DEFAULT_UA
|
||||
})
|
||||
|
||||
@ -238,9 +274,28 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def json_dict_to_request_body(data: Dict[str, Any]) -> str:
|
||||
# Propagate the top-level list if there is only one
|
||||
# item in the object, with an en empty key.
|
||||
if len(data) == 1:
|
||||
[(key, value)] = data.items()
|
||||
if key == '' and isinstance(value, list):
|
||||
data = value
|
||||
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
else:
|
||||
# We need to set data to an empty string to prevent requests
|
||||
# from assigning an empty list to `response.request.data`.
|
||||
data = ''
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def make_request_kwargs(
|
||||
env: Environment,
|
||||
args: argparse.Namespace,
|
||||
base_headers: RequestHeadersDict = None,
|
||||
base_headers: HTTPHeadersDict = None,
|
||||
request_body_read_callback=lambda chunk: chunk
|
||||
) -> dict:
|
||||
"""
|
||||
@ -252,12 +307,7 @@ def make_request_kwargs(
|
||||
data = args.data
|
||||
auto_json = data and not args.form
|
||||
if (args.json or auto_json) and isinstance(data, dict):
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
else:
|
||||
# We need to set data to an empty string to prevent requests
|
||||
# from assigning an empty list to `response.request.data`.
|
||||
data = ''
|
||||
data = json_dict_to_request_body(data)
|
||||
|
||||
# Finalize headers.
|
||||
headers = make_default_headers(args)
|
||||
@ -282,7 +332,8 @@ def make_request_kwargs(
|
||||
'url': args.url,
|
||||
'headers': headers,
|
||||
'data': prepare_request_body(
|
||||
body=data,
|
||||
env,
|
||||
data,
|
||||
body_read_callback=request_body_read_callback,
|
||||
chunked=args.chunked,
|
||||
offline=args.offline,
|
||||
|
@ -1,4 +1,90 @@
|
||||
import sys
|
||||
from typing import Any, Optional, Iterable
|
||||
|
||||
|
||||
is_windows = 'win32' in str(sys.platform).lower()
|
||||
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
# Can be removed once we drop Python <3.8 support.
|
||||
# Taken from `django.utils.functional.cached_property`.
|
||||
class cached_property:
|
||||
"""
|
||||
Decorator that converts a method with a single self argument into a
|
||||
property cached on the instance.
|
||||
|
||||
A cached property can be made out of an existing method:
|
||||
(e.g. ``url = cached_property(get_absolute_url)``).
|
||||
The optional ``name`` argument is obsolete as of Python 3.6 and will be
|
||||
deprecated in Django 4.0 (#30127).
|
||||
"""
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def func(instance):
|
||||
raise TypeError(
|
||||
'Cannot use cached_property instance without calling '
|
||||
'__set_name__() on it.'
|
||||
)
|
||||
|
||||
def __init__(self, func, name=None):
|
||||
self.real_func = func
|
||||
self.__doc__ = getattr(func, '__doc__')
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.name is None:
|
||||
self.name = name
|
||||
self.func = self.real_func
|
||||
elif name != self.name:
|
||||
raise TypeError(
|
||||
"Cannot assign the same cached_property to two different names "
|
||||
"(%r and %r)." % (self.name, name)
|
||||
)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Call the function and put the return value in instance.__dict__ so that
|
||||
subsequent attribute access on the instance returns the cached value
|
||||
instead of calling cached_property.__get__().
|
||||
"""
|
||||
if instance is None:
|
||||
return self
|
||||
res = instance.__dict__[self.name] = self.func(instance)
|
||||
return res
|
||||
|
||||
|
||||
# importlib_metadata was a provisional module, so the APIs changed quite a few times
|
||||
# between 3.8-3.10. It was also not included in the standard library until 3.8, so
|
||||
# we install the backport for <3.8.
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
import importlib.metadata as importlib_metadata
|
||||
else:
|
||||
import importlib_metadata
|
||||
|
||||
|
||||
def find_entry_points(entry_points: Any, group: str) -> Iterable[importlib_metadata.EntryPoint]:
|
||||
if hasattr(entry_points, "select"): # Python 3.10+ / importlib_metadata >= 3.9.0
|
||||
return entry_points.select(group=group)
|
||||
else:
|
||||
return set(entry_points.get(group, ()))
|
||||
|
||||
|
||||
def get_dist_name(entry_point: importlib_metadata.EntryPoint) -> Optional[str]:
|
||||
dist = getattr(entry_point, "dist", None)
|
||||
if dist is not None: # Python 3.10+
|
||||
return dist.name
|
||||
|
||||
match = entry_point.pattern.match(entry_point.value)
|
||||
if not (match and match.group('module')):
|
||||
return None
|
||||
|
||||
package = match.group('module').split('.')[0]
|
||||
try:
|
||||
metadata = importlib_metadata.metadata(package)
|
||||
except importlib_metadata.PackageNotFoundError:
|
||||
return None
|
||||
else:
|
||||
return metadata.get('name')
|
||||
|
@ -5,7 +5,7 @@ from typing import Union
|
||||
|
||||
from . import __version__
|
||||
from .compat import is_windows
|
||||
from .constants import UTF8
|
||||
from .encoding import UTF8
|
||||
|
||||
|
||||
ENV_XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
|
||||
@ -128,3 +128,7 @@ class Config(BaseConfigDict):
|
||||
@property
|
||||
def default_options(self) -> list:
|
||||
return self['default_options']
|
||||
|
||||
@property
|
||||
def plugins_dir(self) -> Path:
|
||||
return Path(self.get('plugins_dir', self.directory / 'plugins')).resolve()
|
||||
|
@ -1,2 +0,0 @@
|
||||
# UTF-8 encoding name
|
||||
UTF8 = 'utf-8'
|
@ -1,7 +1,8 @@
|
||||
import sys
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import IO, Optional
|
||||
from typing import Iterator, IO, Optional
|
||||
|
||||
|
||||
try:
|
||||
@ -11,7 +12,7 @@ except ImportError:
|
||||
|
||||
from .compat import is_windows
|
||||
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
||||
from .constants import UTF8
|
||||
from .encoding import UTF8
|
||||
|
||||
from .utils import repr_dict
|
||||
|
||||
@ -120,6 +121,19 @@ class Environment:
|
||||
self._devnull = open(os.devnull, 'w+')
|
||||
return self._devnull
|
||||
|
||||
@contextmanager
|
||||
def as_silent(self) -> Iterator[None]:
|
||||
original_stdout = self.stdout
|
||||
original_stderr = self.stderr
|
||||
|
||||
try:
|
||||
self.stdout = self.devnull
|
||||
self.stderr = self.devnull
|
||||
yield
|
||||
finally:
|
||||
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')
|
||||
|
143
httpie/core.py
143
httpie/core.py
@ -2,39 +2,40 @@ import argparse
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import List, Optional, Tuple, Union
|
||||
import socket
|
||||
from typing import List, Optional, Union, Callable
|
||||
|
||||
import requests
|
||||
from pygments import __version__ as pygments_version
|
||||
from requests import __version__ as requests_version
|
||||
|
||||
from . import __version__ as httpie_version
|
||||
from .cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
|
||||
from .cli.constants import OUT_REQ_BODY
|
||||
from .cli.nested_json import HTTPieSyntaxError
|
||||
from .client import collect_messages
|
||||
from .context import Environment
|
||||
from .downloads import Downloader
|
||||
from .models import (
|
||||
RequestsMessageKind,
|
||||
OutputOptions,
|
||||
)
|
||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
||||
from .plugins.registry import plugin_manager
|
||||
from .status import ExitStatus, http_status_to_exit_status
|
||||
from .utils import unwrap_context
|
||||
|
||||
|
||||
# noinspection PyDefaultArgument
|
||||
def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitStatus:
|
||||
"""
|
||||
The main function.
|
||||
|
||||
Pre-process args, handle some special types of invocations,
|
||||
and run the main program with error handling.
|
||||
|
||||
Return exit status code.
|
||||
|
||||
"""
|
||||
def raw_main(
|
||||
parser: argparse.ArgumentParser,
|
||||
main_program: Callable[[argparse.Namespace, Environment], ExitStatus],
|
||||
args: List[Union[str, bytes]] = sys.argv,
|
||||
env: Environment = Environment()
|
||||
) -> ExitStatus:
|
||||
program_name, *args = args
|
||||
env.program_name = os.path.basename(program_name)
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
plugin_manager.load_installed_plugins()
|
||||
|
||||
from .cli.definition import parser
|
||||
plugin_manager.load_installed_plugins(env.config.plugins_dir)
|
||||
|
||||
if env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
@ -42,6 +43,21 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
||||
include_debug_info = '--debug' in args
|
||||
include_traceback = include_debug_info or '--traceback' in args
|
||||
|
||||
def handle_generic_error(e, annotation=None):
|
||||
msg = str(e)
|
||||
if hasattr(e, 'request'):
|
||||
request = e.request
|
||||
if hasattr(request, 'url'):
|
||||
msg = (
|
||||
f'{msg} while doing a {request.method}'
|
||||
f' request to URL: {request.url}'
|
||||
)
|
||||
if annotation:
|
||||
msg += annotation
|
||||
env.log_error(f'{type(e).__name__}: {msg}')
|
||||
if include_traceback:
|
||||
raise
|
||||
|
||||
if include_debug_info:
|
||||
print_debug_info(env)
|
||||
if args == ['--debug']:
|
||||
@ -54,6 +70,11 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
||||
args=args,
|
||||
env=env,
|
||||
)
|
||||
except HTTPieSyntaxError as exc:
|
||||
env.stderr.write(str(exc) + "\n")
|
||||
if include_traceback:
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
except KeyboardInterrupt:
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
@ -67,7 +88,7 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
||||
exit_status = ExitStatus.ERROR
|
||||
else:
|
||||
try:
|
||||
exit_status = program(
|
||||
exit_status = main_program(
|
||||
args=parsed_args,
|
||||
env=env,
|
||||
)
|
||||
@ -91,38 +112,50 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
||||
f'Too many redirects'
|
||||
f' (--max-redirects={parsed_args.max_redirects}).'
|
||||
)
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
annotation = None
|
||||
original_exc = unwrap_context(exc)
|
||||
if isinstance(original_exc, socket.gaierror):
|
||||
if original_exc.errno == socket.EAI_AGAIN:
|
||||
annotation = '\nCouldn’t connect to a DNS server. Please check your connection and try again.'
|
||||
elif original_exc.errno == socket.EAI_NONAME:
|
||||
annotation = '\nCouldn’t resolve the given hostname. Please check the URL and try again.'
|
||||
propagated_exc = original_exc
|
||||
else:
|
||||
propagated_exc = exc
|
||||
|
||||
handle_generic_error(propagated_exc, annotation=annotation)
|
||||
exit_status = ExitStatus.ERROR
|
||||
except Exception as e:
|
||||
# TODO: Further distinction between expected and unexpected errors.
|
||||
msg = str(e)
|
||||
if hasattr(e, 'request'):
|
||||
request = e.request
|
||||
if hasattr(request, 'url'):
|
||||
msg = (
|
||||
f'{msg} while doing a {request.method}'
|
||||
f' request to URL: {request.url}'
|
||||
)
|
||||
env.log_error(f'{type(e).__name__}: {msg}')
|
||||
if include_traceback:
|
||||
raise
|
||||
handle_generic_error(e)
|
||||
exit_status = ExitStatus.ERROR
|
||||
|
||||
return exit_status
|
||||
|
||||
|
||||
def get_output_options(
|
||||
args: argparse.Namespace,
|
||||
message: Union[requests.PreparedRequest, requests.Response]
|
||||
) -> Tuple[bool, bool]:
|
||||
return {
|
||||
requests.PreparedRequest: (
|
||||
OUT_REQ_HEAD in args.output_options,
|
||||
OUT_REQ_BODY in args.output_options,
|
||||
),
|
||||
requests.Response: (
|
||||
OUT_RESP_HEAD in args.output_options,
|
||||
OUT_RESP_BODY in args.output_options,
|
||||
),
|
||||
}[type(message)]
|
||||
def main(
|
||||
args: List[Union[str, bytes]] = sys.argv,
|
||||
env: Environment = Environment()
|
||||
) -> ExitStatus:
|
||||
"""
|
||||
The main function.
|
||||
|
||||
Pre-process args, handle some special types of invocations,
|
||||
and run the main program with error handling.
|
||||
|
||||
Return exit status code.
|
||||
|
||||
"""
|
||||
|
||||
from .cli.definition import parser
|
||||
|
||||
return raw_main(
|
||||
parser=parser,
|
||||
main_program=program,
|
||||
args=args,
|
||||
env=env
|
||||
)
|
||||
|
||||
|
||||
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
@ -153,31 +186,32 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
msg.is_body_upload_chunk = True
|
||||
msg.body = chunk
|
||||
msg.headers = initial_request.headers
|
||||
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
|
||||
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)
|
||||
|
||||
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.pre_request(args.headers)
|
||||
messages = collect_messages(args=args, config_dir=env.config.directory,
|
||||
messages = collect_messages(env, args=args,
|
||||
request_body_read_callback=request_body_read_callback)
|
||||
force_separator = False
|
||||
prev_with_body = False
|
||||
|
||||
# Process messages as they’re generated
|
||||
for message in messages:
|
||||
is_request = isinstance(message, requests.PreparedRequest)
|
||||
with_headers, with_body = get_output_options(args=args, message=message)
|
||||
do_write_body = with_body
|
||||
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
|
||||
output_options = OutputOptions.from_message(message, args.output_options)
|
||||
|
||||
do_write_body = output_options.body
|
||||
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty):
|
||||
# Separate after a previous message with body, if needed. See test_tokens.py.
|
||||
separate()
|
||||
force_separator = False
|
||||
if is_request:
|
||||
if output_options.kind is RequestsMessageKind.REQUEST:
|
||||
if not initial_request:
|
||||
initial_request = message
|
||||
if with_body:
|
||||
if output_options.body:
|
||||
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
||||
do_write_body = not is_streamed_upload
|
||||
force_separator = is_streamed_upload and env.stdout_isatty
|
||||
@ -185,11 +219,12 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
final_response = message
|
||||
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):
|
||||
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, with_headers=with_headers,
|
||||
with_body=do_write_body)
|
||||
prev_with_body = with_body
|
||||
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
|
||||
body=do_write_body
|
||||
))
|
||||
prev_with_body = output_options.body
|
||||
|
||||
# Cleanup
|
||||
if force_separator:
|
||||
@ -227,6 +262,8 @@ def print_debug_info(env: Environment):
|
||||
])
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(env))
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(plugin_manager))
|
||||
env.stderr.write('\n')
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ from urllib.parse import urlsplit
|
||||
|
||||
import requests
|
||||
|
||||
from .models import HTTPResponse
|
||||
from .models import HTTPResponse, OutputOptions
|
||||
from .output.streams import RawStream
|
||||
from .utils import humanize_bytes
|
||||
|
||||
@ -266,12 +266,11 @@ class Downloader:
|
||||
total_size=total_size
|
||||
)
|
||||
|
||||
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
||||
stream = RawStream(
|
||||
msg=HTTPResponse(final_response),
|
||||
with_headers=False,
|
||||
with_body=True,
|
||||
output_options=output_options,
|
||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||
chunk_size=1024 * 8
|
||||
)
|
||||
|
||||
self._progress_reporter.output.write(
|
||||
@ -324,7 +323,7 @@ class Downloader:
|
||||
content_type=final_response.headers.get('Content-Type'),
|
||||
)
|
||||
unique_filename = get_unique_filename(filename)
|
||||
return open(unique_filename, mode='a+b')
|
||||
return open(unique_filename, buffering=0, mode='a+b')
|
||||
|
||||
|
||||
class DownloadStatus:
|
||||
|
50
httpie/encoding.py
Normal file
50
httpie/encoding.py
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import Union, Tuple
|
||||
|
||||
from charset_normalizer import from_bytes
|
||||
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
|
||||
|
||||
UTF8 = 'utf-8'
|
||||
|
||||
ContentBytes = Union[bytearray, bytes]
|
||||
|
||||
|
||||
def detect_encoding(content: ContentBytes) -> str:
|
||||
"""
|
||||
We default to UTF-8 if text too short, because the detection
|
||||
can return a random encoding leading to confusing results
|
||||
given the `charset_normalizer` version (< 2.0.5).
|
||||
|
||||
>>> too_short = ']"foo"'
|
||||
>>> detected = from_bytes(too_short.encode()).best().encoding
|
||||
>>> detected
|
||||
'ascii'
|
||||
>>> too_short.encode().decode(detected)
|
||||
']"foo"'
|
||||
"""
|
||||
encoding = UTF8
|
||||
if len(content) > TOO_SMALL_SEQUENCE:
|
||||
match = from_bytes(bytes(content)).best()
|
||||
if match:
|
||||
encoding = match.encoding
|
||||
return encoding
|
||||
|
||||
|
||||
def smart_decode(content: ContentBytes, encoding: str) -> Tuple[str, str]:
|
||||
"""Decode `content` using the given `encoding`.
|
||||
If no `encoding` is provided, the best effort is to guess it from `content`.
|
||||
|
||||
Unicode errors are replaced.
|
||||
|
||||
"""
|
||||
if not encoding:
|
||||
encoding = detect_encoding(content)
|
||||
return content.decode(encoding, 'replace'), encoding
|
||||
|
||||
|
||||
def smart_encode(content: str, encoding: str) -> bytes:
|
||||
"""Encode `content` using the given `encoding`.
|
||||
|
||||
Unicode errors are replaced.
|
||||
|
||||
"""
|
||||
return content.encode(encoding, 'replace')
|
0
httpie/manager/__init__.py
Normal file
0
httpie/manager/__init__.py
Normal file
61
httpie/manager/__main__.py
Normal file
61
httpie/manager/__main__.py
Normal file
@ -0,0 +1,61 @@
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.manager.cli import parser
|
||||
from httpie.manager.core import MSG_COMMAND_CONFUSION, program as main_program
|
||||
|
||||
|
||||
def is_http_command(args: List[Union[str, bytes]], env: Environment) -> bool:
|
||||
"""Check whether http/https parser can parse the arguments."""
|
||||
|
||||
from httpie.cli.definition import parser as http_parser
|
||||
from httpie.manager.cli import COMMANDS
|
||||
|
||||
# If the user already selected a top-level sub-command, never
|
||||
# show the http/https version. E.g httpie plugins pie.dev/post
|
||||
if len(args) >= 1 and args[0] in COMMANDS:
|
||||
return False
|
||||
|
||||
with env.as_silent():
|
||||
try:
|
||||
http_parser.parse_args(env=env, args=args)
|
||||
except (Exception, SystemExit):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def main(args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()) -> ExitStatus:
|
||||
from httpie.core import raw_main
|
||||
|
||||
try:
|
||||
return raw_main(
|
||||
parser=parser,
|
||||
main_program=main_program,
|
||||
args=args,
|
||||
env=env
|
||||
)
|
||||
except argparse.ArgumentError:
|
||||
program_args = args[1:]
|
||||
if is_http_command(program_args, env):
|
||||
env.stderr.write(MSG_COMMAND_CONFUSION.format(args=' '.join(program_args)) + "\n")
|
||||
|
||||
return ExitStatus.ERROR
|
||||
|
||||
|
||||
def program():
|
||||
try:
|
||||
exit_status = main()
|
||||
except KeyboardInterrupt:
|
||||
from httpie.status import ExitStatus
|
||||
exit_status = ExitStatus.ERROR_CTRL_C
|
||||
|
||||
return exit_status
|
||||
|
||||
|
||||
if __name__ == '__main__': # pragma: nocover
|
||||
sys.exit(program())
|
112
httpie/manager/cli.py
Normal file
112
httpie/manager/cli.py
Normal file
@ -0,0 +1,112 @@
|
||||
from textwrap import dedent
|
||||
from httpie.cli.argparser import HTTPieManagerArgumentParser
|
||||
from httpie import __version__
|
||||
|
||||
COMMANDS = {
|
||||
'plugins': {
|
||||
'help': 'Manage HTTPie plugins.',
|
||||
'install': [
|
||||
'Install the given targets from PyPI '
|
||||
'or from a local paths.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
'upgrade': [
|
||||
'Upgrade the given plugins',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to upgrade'
|
||||
}
|
||||
],
|
||||
'uninstall': [
|
||||
'Uninstall the given HTTPie plugins.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
'list': [
|
||||
'List all installed HTTPie plugins.'
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def missing_subcommand(*args) -> str:
|
||||
base = COMMANDS
|
||||
for arg in args:
|
||||
base = base[arg]
|
||||
|
||||
assert isinstance(base, dict)
|
||||
subcommands = ', '.join(map(repr, base.keys()))
|
||||
return f'Please specify one of these: {subcommands}'
|
||||
|
||||
|
||||
def generate_subparsers(root, parent_parser, definitions):
|
||||
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)
|
||||
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)
|
||||
continue
|
||||
|
||||
for argument in properties:
|
||||
command_parser.add_argument(**argument)
|
||||
|
||||
|
||||
parser = HTTPieManagerArgumentParser(
|
||||
prog='httpie',
|
||||
description=dedent(
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help='''
|
||||
Show version and exit.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
generate_subparsers(parser, parser, COMMANDS)
|
33
httpie/manager/core.py
Normal file
33
httpie/manager/core.py
Normal file
@ -0,0 +1,33 @@
|
||||
import argparse
|
||||
|
||||
from httpie.context import Environment
|
||||
from httpie.manager.plugins import PluginInstaller
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.manager.cli import missing_subcommand, parser
|
||||
|
||||
MSG_COMMAND_CONFUSION = '''\
|
||||
This command is only for managing HTTPie plugins.
|
||||
To send a request, please use the http/https commands:
|
||||
|
||||
$ http {args}
|
||||
|
||||
$ https {args}
|
||||
'''
|
||||
|
||||
# noinspection PyStringFormat
|
||||
MSG_NAKED_INVOCATION = f'''\
|
||||
{missing_subcommand()}
|
||||
|
||||
{MSG_COMMAND_CONFUSION}
|
||||
'''.rstrip("\n").format(args='POST pie.dev/post hello=world')
|
||||
|
||||
|
||||
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 ExitStatus.SUCCESS
|
250
httpie/manager/plugins.py
Normal file
250
httpie/manager/plugins.py
Normal file
@ -0,0 +1,250 @@
|
||||
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 httpie.manager.cli import parser, missing_subcommand
|
||||
from httpie.compat import importlib_metadata, get_dist_name
|
||||
from httpie.context import Environment
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.utils import as_site
|
||||
|
||||
PEP_503 = re.compile(r"[-_.]+")
|
||||
|
||||
|
||||
class PluginInstaller:
|
||||
|
||||
def __init__(self, env: Environment, debug: bool = False) -> None:
|
||||
self.env = env
|
||||
self.dir = env.config.plugins_dir
|
||||
self.debug = debug
|
||||
|
||||
self.setup_plugins_dir()
|
||||
|
||||
def setup_plugins_dir(self) -> None:
|
||||
try:
|
||||
self.dir.mkdir(
|
||||
exist_ok=True,
|
||||
parents=True
|
||||
)
|
||||
except OSError:
|
||||
self.env.stderr.write(
|
||||
f'Couldn\'t create "{self.dir!s}"'
|
||||
' directory for plugin installation.'
|
||||
' Please re-check the permissions for that directory,'
|
||||
' and if needed, allow write-access.'
|
||||
)
|
||||
raise
|
||||
|
||||
def fail(
|
||||
self,
|
||||
command: str,
|
||||
target: Optional[str] = None,
|
||||
reason: Optional[str] = None
|
||||
) -> ExitStatus:
|
||||
message = f'Can\'t {command}'
|
||||
if target:
|
||||
message += f' {target!r}'
|
||||
if reason:
|
||||
message += f': {reason}'
|
||||
|
||||
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
|
||||
]:
|
||||
pip_args = [
|
||||
'install',
|
||||
f'--prefix={self.dir}',
|
||||
'--no-warn-script-location',
|
||||
]
|
||||
if mode == 'upgrade':
|
||||
pip_args.append('--upgrade')
|
||||
|
||||
try:
|
||||
process = self.pip(
|
||||
*pip_args,
|
||||
*targets,
|
||||
**process_options,
|
||||
)
|
||||
except subprocess.CalledProcessError as 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(textwrap.indent(' ', stderr))
|
||||
|
||||
last_line = stderr.strip().splitlines()[-1]
|
||||
severity, _, message = last_line.partition(': ')
|
||||
if severity == 'ERROR':
|
||||
reason = message
|
||||
|
||||
stdout = error.stdout
|
||||
exit_status = self.fail(mode, ', '.join(targets), reason)
|
||||
else:
|
||||
stdout = process.stdout
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
|
||||
return stdout, exit_status
|
||||
|
||||
def install(self, targets: List[str]) -> ExitStatus:
|
||||
self.env.stdout.write(f"Installing {', '.join(targets)}...\n")
|
||||
self.env.stdout.flush()
|
||||
_, exit_status = self._install(targets)
|
||||
return exit_status
|
||||
|
||||
def _clear_metadata(self, targets: List[str]) -> None:
|
||||
# Due to an outstanding pip problem[0], we have to get rid of
|
||||
# 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 target in targets:
|
||||
name, _, version = target.rpartition('-')
|
||||
name = PEP_503.sub("-", name).lower().replace('-', '_')
|
||||
if name not in result_deps:
|
||||
continue
|
||||
|
||||
for result_version, meta_path in result_deps[name]:
|
||||
if version != result_version:
|
||||
shutil.rmtree(meta_path)
|
||||
|
||||
def upgrade(self, targets: List[str]) -> ExitStatus:
|
||||
self.env.stdout.write(f"Upgrading {', '.join(targets)}...\n")
|
||||
self.env.stdout.flush()
|
||||
|
||||
raw_stdout, exit_status = self._install(
|
||||
targets,
|
||||
mode='upgrade',
|
||||
stdout=subprocess.PIPE
|
||||
)
|
||||
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:])
|
||||
|
||||
def _uninstall(self, target: str) -> Optional[ExitStatus]:
|
||||
try:
|
||||
distribution = importlib_metadata.distribution(target)
|
||||
except importlib_metadata.PackageNotFoundError:
|
||||
return self.fail('uninstall', target, 'package is not installed')
|
||||
|
||||
base_dir = Path(distribution.locate_file('.')).resolve()
|
||||
if self.dir not in base_dir.parents:
|
||||
# If the package is installed somewhere else (e.g on the site packages
|
||||
# of the real python interpreter), than that means this package is not
|
||||
# installed through us.
|
||||
return self.fail('uninstall', target,
|
||||
'package is not installed through httpie plugins'
|
||||
' interface')
|
||||
|
||||
files = distribution.files
|
||||
if files is None:
|
||||
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
|
||||
# in a proper shape).
|
||||
for file in files:
|
||||
with suppress(FileNotFoundError):
|
||||
os.unlink(distribution.locate_file(file))
|
||||
|
||||
metadata_path = getattr(distribution, '_path', None)
|
||||
if (
|
||||
metadata_path
|
||||
and metadata_path.exists()
|
||||
and not any(metadata_path.iterdir())
|
||||
):
|
||||
metadata_path.rmdir()
|
||||
|
||||
self.env.stdout.write(f'Successfully uninstalled {target}\n')
|
||||
|
||||
def uninstall(self, targets: List[str]) -> ExitStatus:
|
||||
# Unfortunately uninstall doesn't work with custom pip schemes. See:
|
||||
# - https://github.com/pypa/pip/issues/5595
|
||||
# - https://github.com/pypa/pip/issues/4575
|
||||
# so we have to implement our own uninstalling logic. Which works
|
||||
# on top of the importlib_metadata.
|
||||
|
||||
exit_code = ExitStatus.SUCCESS
|
||||
for target in targets:
|
||||
exit_code |= self._uninstall(target) or ExitStatus.SUCCESS
|
||||
return ExitStatus(exit_code)
|
||||
|
||||
def list(self) -> None:
|
||||
from httpie.plugins.registry import plugin_manager
|
||||
|
||||
known_plugins = defaultdict(list)
|
||||
|
||||
for entry_point in plugin_manager.iter_entry_points(self.dir):
|
||||
ep_info = (entry_point.group, entry_point.name)
|
||||
ep_name = get_dist_name(entry_point) or entry_point.module
|
||||
known_plugins[ep_name].append(ep_info)
|
||||
|
||||
for plugin, entry_points in known_plugins.items():
|
||||
self.env.stdout.write(plugin)
|
||||
|
||||
version = importlib_metadata.version(plugin)
|
||||
if version is not None:
|
||||
self.env.stdout.write(f' ({version})')
|
||||
self.env.stdout.write('\n')
|
||||
|
||||
for group, entry_point in sorted(entry_points):
|
||||
self.env.stdout.write(f' {entry_point} ({group})\n')
|
||||
|
||||
def run(
|
||||
self,
|
||||
action: Optional[str],
|
||||
args: argparse.Namespace,
|
||||
) -> ExitStatus:
|
||||
from httpie.plugins.manager import enable_plugins
|
||||
|
||||
if action is None:
|
||||
parser.error(missing_subcommand('plugins'))
|
||||
|
||||
with enable_plugins(self.dir):
|
||||
if action == 'install':
|
||||
status = self.install(args.targets)
|
||||
elif action == 'upgrade':
|
||||
status = self.upgrade(args.targets)
|
||||
elif action == 'uninstall':
|
||||
status = self.uninstall(args.targets)
|
||||
elif action == 'list':
|
||||
status = self.list()
|
||||
|
||||
return status or ExitStatus.SUCCESS
|
128
httpie/models.py
128
httpie/models.py
@ -1,39 +1,48 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Iterable, Optional
|
||||
import requests
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Iterable, Union, NamedTuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .constants import UTF8
|
||||
from .utils import split_cookies
|
||||
from .cli.constants import (
|
||||
OUT_REQ_BODY,
|
||||
OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY,
|
||||
OUT_RESP_HEAD,
|
||||
OUT_RESP_META
|
||||
)
|
||||
from .compat import cached_property
|
||||
from .utils import split_cookies, parse_content_type_header
|
||||
|
||||
|
||||
class HTTPMessage(metaclass=ABCMeta):
|
||||
class HTTPMessage:
|
||||
"""Abstract class for HTTP messages."""
|
||||
|
||||
def __init__(self, orig):
|
||||
self._orig = orig
|
||||
|
||||
@abstractmethod
|
||||
def iter_body(self, chunk_size: int) -> Iterable[bytes]:
|
||||
"""Return an iterator over the body."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def iter_lines(self, chunk_size: int) -> Iterable[bytes]:
|
||||
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def headers(self) -> str:
|
||||
"""Return a `str` with the message's headers."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def encoding(self) -> Optional[str]:
|
||||
"""Return a `str` with the message's encoding, if known."""
|
||||
def metadata(self) -> str:
|
||||
"""Return metadata about the current message."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def body(self) -> bytes:
|
||||
"""Return a `bytes` with the message's body."""
|
||||
raise NotImplementedError()
|
||||
@cached_property
|
||||
def encoding(self) -> str:
|
||||
ct, params = parse_content_type_header(self.content_type)
|
||||
return params.get('charset', '')
|
||||
|
||||
@property
|
||||
def content_type(self) -> str:
|
||||
@ -78,19 +87,20 @@ class HTTPResponse(HTTPMessage):
|
||||
)
|
||||
headers.extend(
|
||||
f'Set-Cookie: {cookie}'
|
||||
for cookie in split_cookies(original.headers.get('Set-Cookie'))
|
||||
for header, value in original.headers.items()
|
||||
for cookie in split_cookies(value)
|
||||
if header == 'Set-Cookie'
|
||||
)
|
||||
return '\r\n'.join(headers)
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return self._orig.encoding or UTF8
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
# Only now the response body is fetched.
|
||||
# Shouldn't be touched unless the body is actually needed.
|
||||
return self._orig.content
|
||||
def metadata(self) -> str:
|
||||
data = {}
|
||||
data['Elapsed time'] = str(self._orig.elapsed.total_seconds()) + 's'
|
||||
return '\n'.join(
|
||||
f'{key}: {value}'
|
||||
for key, value in data.items()
|
||||
)
|
||||
|
||||
|
||||
class HTTPRequest(HTTPMessage):
|
||||
@ -112,7 +122,7 @@ class HTTPRequest(HTTPMessage):
|
||||
query=f'?{url.query}' if url.query else ''
|
||||
)
|
||||
|
||||
headers = dict(self._orig.headers)
|
||||
headers = self._orig.headers.copy()
|
||||
if 'Host' not in self._orig.headers:
|
||||
headers['Host'] = url.netloc.split('@')[-1]
|
||||
|
||||
@ -125,10 +135,6 @@ class HTTPRequest(HTTPMessage):
|
||||
headers = '\r\n'.join(headers).strip()
|
||||
return headers
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return UTF8
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
body = self._orig.body
|
||||
@ -136,3 +142,67 @@ class HTTPRequest(HTTPMessage):
|
||||
# Happens with JSON/form request data parsed from the command line.
|
||||
body = body.encode()
|
||||
return body or b''
|
||||
|
||||
|
||||
RequestsMessage = Union[requests.PreparedRequest, requests.Response]
|
||||
|
||||
|
||||
class RequestsMessageKind(Enum):
|
||||
REQUEST = auto()
|
||||
RESPONSE = auto()
|
||||
|
||||
|
||||
def infer_requests_message_kind(message: RequestsMessage) -> RequestsMessageKind:
|
||||
if isinstance(message, requests.PreparedRequest):
|
||||
return RequestsMessageKind.REQUEST
|
||||
elif isinstance(message, requests.Response):
|
||||
return RequestsMessageKind.RESPONSE
|
||||
else:
|
||||
raise TypeError(f"Unexpected message type: {type(message).__name__}")
|
||||
|
||||
|
||||
OPTION_TO_PARAM = {
|
||||
RequestsMessageKind.REQUEST: {
|
||||
'headers': OUT_REQ_HEAD,
|
||||
'body': OUT_REQ_BODY,
|
||||
},
|
||||
RequestsMessageKind.RESPONSE: {
|
||||
'headers': OUT_RESP_HEAD,
|
||||
'body': OUT_RESP_BODY,
|
||||
'meta': OUT_RESP_META
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class OutputOptions(NamedTuple):
|
||||
kind: RequestsMessageKind
|
||||
headers: bool
|
||||
body: bool
|
||||
meta: bool = False
|
||||
|
||||
def any(self):
|
||||
return (
|
||||
self.headers
|
||||
or self.body
|
||||
or self.meta
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_message(
|
||||
cls,
|
||||
message: RequestsMessage,
|
||||
raw_args: str = '',
|
||||
**kwargs
|
||||
):
|
||||
kind = infer_requests_message_kind(message)
|
||||
|
||||
options = {
|
||||
option: param in raw_args
|
||||
for option, param in OPTION_TO_PARAM[kind].items()
|
||||
}
|
||||
options.update(kwargs)
|
||||
|
||||
return cls(
|
||||
kind=kind,
|
||||
**options
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import json
|
||||
from typing import Optional, Type
|
||||
from typing import Optional, Type, Tuple
|
||||
|
||||
import pygments.formatters
|
||||
import pygments.lexer
|
||||
import pygments.lexers
|
||||
import pygments.style
|
||||
@ -9,11 +10,14 @@ import pygments.token
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||
from pygments.lexer import Lexer
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from pygments.lexers.special import TextLexer
|
||||
from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from ...compat import is_windows
|
||||
from ..lexers.json import EnhancedJsonLexer
|
||||
from ..lexers.metadata import MetadataLexer
|
||||
from ..ui.palette import SHADE_NAMES, get_color
|
||||
from ...context import Environment
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
@ -21,14 +25,15 @@ from ...plugins import FormatterPlugin
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
DEFAULT_STYLE = AUTO_STYLE
|
||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
||||
if is_windows:
|
||||
# Colors on Windows via colorama don't look that
|
||||
# great and fruity seems to give the best result there.
|
||||
DEFAULT_STYLE = 'fruity'
|
||||
|
||||
AVAILABLE_STYLES = set(pygments.styles.get_all_styles())
|
||||
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
|
||||
AVAILABLE_STYLES.add(AUTO_STYLE)
|
||||
BUNDLED_STYLES = {
|
||||
SOLARIZED_STYLE,
|
||||
AUTO_STYLE
|
||||
}
|
||||
|
||||
|
||||
def get_available_styles():
|
||||
return BUNDLED_STYLES | set(pygments.styles.get_all_styles())
|
||||
|
||||
|
||||
class ColorFormatter(FormatterPlugin):
|
||||
@ -40,6 +45,7 @@ class ColorFormatter(FormatterPlugin):
|
||||
|
||||
"""
|
||||
group_name = 'colors'
|
||||
metadata_lexer = MetadataLexer()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -58,22 +64,24 @@ class ColorFormatter(FormatterPlugin):
|
||||
has_256_colors = env.colors == 256
|
||||
if use_auto_style or not has_256_colors:
|
||||
http_lexer = PygmentsHttpLexer()
|
||||
formatter = TerminalFormatter()
|
||||
body_formatter = header_formatter = TerminalFormatter()
|
||||
precise = False
|
||||
else:
|
||||
http_lexer = SimplifiedHTTPLexer()
|
||||
formatter = Terminal256Formatter(
|
||||
style=self.get_style_class(color_scheme)
|
||||
)
|
||||
from ..lexers.http import SimplifiedHTTPLexer
|
||||
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
|
||||
http_lexer = SimplifiedHTTPLexer(precise=precise)
|
||||
|
||||
self.explicit_json = explicit_json # --json
|
||||
self.formatter = formatter
|
||||
self.header_formatter = header_formatter
|
||||
self.body_formatter = body_formatter
|
||||
self.http_lexer = http_lexer
|
||||
self.metadata_lexer = MetadataLexer(precise=precise)
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
return pygments.highlight(
|
||||
code=headers,
|
||||
lexer=self.http_lexer,
|
||||
formatter=self.formatter,
|
||||
formatter=self.header_formatter,
|
||||
).strip()
|
||||
|
||||
def format_body(self, body: str, mime: str) -> str:
|
||||
@ -82,10 +90,17 @@ class ColorFormatter(FormatterPlugin):
|
||||
body = pygments.highlight(
|
||||
code=body,
|
||||
lexer=lexer,
|
||||
formatter=self.formatter,
|
||||
formatter=self.body_formatter,
|
||||
)
|
||||
return body
|
||||
|
||||
def format_metadata(self, metadata: str) -> str:
|
||||
return pygments.highlight(
|
||||
code=metadata,
|
||||
lexer=self.metadata_lexer,
|
||||
formatter=self.header_formatter,
|
||||
).strip()
|
||||
|
||||
def get_lexer_for_body(
|
||||
self, mime: str,
|
||||
body: str
|
||||
@ -96,6 +111,25 @@ class ColorFormatter(FormatterPlugin):
|
||||
body=body,
|
||||
)
|
||||
|
||||
def get_formatters(self, color_scheme: str) -> Tuple[
|
||||
pygments.formatter.Formatter,
|
||||
pygments.formatter.Formatter,
|
||||
bool
|
||||
]:
|
||||
if color_scheme in PIE_STYLES:
|
||||
header_style, body_style = PIE_STYLES[color_scheme]
|
||||
precise = True
|
||||
else:
|
||||
header_style = self.get_style_class(color_scheme)
|
||||
body_style = header_style
|
||||
precise = False
|
||||
|
||||
return (
|
||||
Terminal256Formatter(style=header_style),
|
||||
Terminal256Formatter(style=body_style),
|
||||
precise
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
|
||||
try:
|
||||
@ -151,57 +185,14 @@ def get_lexer(
|
||||
else:
|
||||
lexer = pygments.lexers.get_lexer_by_name('json')
|
||||
|
||||
# Use our own JSON lexer: it supports JSON bodies preceded by non-JSON data
|
||||
# as well as legit JSON bodies.
|
||||
if isinstance(lexer, JsonLexer):
|
||||
lexer = EnhancedJsonLexer()
|
||||
|
||||
return lexer
|
||||
|
||||
|
||||
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
"""Simplified HTTP lexer for Pygments.
|
||||
|
||||
It only operates on headers and provides a stronger contrast between
|
||||
their names and values than the original one bundled with Pygments
|
||||
(:class:`pygments.lexers.text import HttpLexer`), especially when
|
||||
Solarized color scheme is used.
|
||||
|
||||
"""
|
||||
name = 'HTTP'
|
||||
aliases = ['http']
|
||||
filenames = ['*.http']
|
||||
tokens = {
|
||||
'root': [
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Name.Function,
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Namespace,
|
||||
pygments.token.Text,
|
||||
pygments.token.Keyword.Reserved,
|
||||
pygments.token.Operator,
|
||||
pygments.token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Keyword.Reserved, # 'HTTP'
|
||||
pygments.token.Operator, # '/'
|
||||
pygments.token.Number, # Version
|
||||
pygments.token.Text,
|
||||
pygments.token.Number, # Status code
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Exception, # Reason
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Attribute, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
pygments.token.String # Value
|
||||
))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Solarized256Style(pygments.style.Style):
|
||||
"""
|
||||
solarized256
|
||||
@ -272,3 +263,124 @@ class Solarized256Style(pygments.style.Style):
|
||||
pygments.token.Token: BASE1,
|
||||
pygments.token.Token.Other: ORANGE,
|
||||
}
|
||||
|
||||
|
||||
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',
|
||||
|
||||
# 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',
|
||||
|
||||
# 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',
|
||||
|
||||
# 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',
|
||||
}
|
||||
|
||||
PIE_BODY_STYLE = {
|
||||
# {}[]:
|
||||
pygments.token.Punctuation: 'grey',
|
||||
|
||||
# Keys
|
||||
pygments.token.Name.Tag: 'pink',
|
||||
|
||||
# Values
|
||||
pygments.token.Literal.String: 'green',
|
||||
pygments.token.Literal.String.Double: 'green',
|
||||
pygments.token.Literal.Number: 'aqua',
|
||||
pygments.token.Keyword: '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',
|
||||
}
|
||||
|
||||
|
||||
def make_style(name, raw_styles, shade):
|
||||
def format_value(value):
|
||||
return ' '.join(
|
||||
get_color(part, shade) or part
|
||||
for part in value.split()
|
||||
)
|
||||
|
||||
bases = (pygments.style.Style,)
|
||||
data = {
|
||||
'styles': {
|
||||
key: format_value(value)
|
||||
for key, value in raw_styles.items()
|
||||
}
|
||||
}
|
||||
return type(name, bases, data)
|
||||
|
||||
|
||||
def make_styles():
|
||||
styles = {}
|
||||
|
||||
for shade, name in SHADE_NAMES.items():
|
||||
styles[name] = [
|
||||
make_style(name, style_map, shade)
|
||||
for style_name, style_map in [
|
||||
(f'Pie{name}HeaderStyle', PIE_HEADER_STYLE),
|
||||
(f'Pie{name}BodyStyle', PIE_BODY_STYLE),
|
||||
]
|
||||
]
|
||||
|
||||
return styles
|
||||
|
||||
|
||||
PIE_STYLES = make_styles()
|
||||
BUNDLED_STYLES |= PIE_STYLES.keys()
|
||||
|
@ -17,15 +17,16 @@ class JSONFormatter(FormatterPlugin):
|
||||
]
|
||||
if (self.kwargs['explicit_json']
|
||||
or any(token in mime for token in maybe_json)):
|
||||
from ..utils import load_prefixed_json
|
||||
try:
|
||||
obj = json.loads(body)
|
||||
data_prefix, json_obj = load_prefixed_json(body)
|
||||
except ValueError:
|
||||
pass # Invalid JSON, ignore.
|
||||
else:
|
||||
# Indent, sort keys by name, and avoid
|
||||
# unicode escapes to improve readability.
|
||||
body = json.dumps(
|
||||
obj=obj,
|
||||
body = data_prefix + json.dumps(
|
||||
obj=json_obj,
|
||||
sort_keys=self.format_options['json']['sort_keys'],
|
||||
ensure_ascii=False,
|
||||
indent=self.format_options['json']['indent']
|
||||
|
@ -1,34 +1,53 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ...constants import UTF8
|
||||
from ...encoding import UTF8
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xml.dom.minidom import Document
|
||||
|
||||
|
||||
XML_DECLARATION_OPEN = '<?xml'
|
||||
XML_DECLARATION_CLOSE = '?>'
|
||||
|
||||
|
||||
def parse_xml(data: str) -> 'Document':
|
||||
"""Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object."""
|
||||
from defusedxml.minidom import parseString
|
||||
return parseString(data)
|
||||
|
||||
|
||||
def parse_declaration(raw_body: str) -> Optional[str]:
|
||||
body = raw_body.strip()
|
||||
# XMLDecl ::= '<?xml' DECL_CONTENT '?>'
|
||||
if body.startswith(XML_DECLARATION_OPEN):
|
||||
end = body.find(XML_DECLARATION_CLOSE)
|
||||
if end != -1:
|
||||
return body[:end + len(XML_DECLARATION_CLOSE)]
|
||||
|
||||
|
||||
def pretty_xml(document: 'Document',
|
||||
declaration: Optional[str] = None,
|
||||
encoding: Optional[str] = UTF8,
|
||||
indent: int = 2,
|
||||
standalone: Optional[bool] = None) -> str:
|
||||
indent: int = 2) -> str:
|
||||
"""Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string."""
|
||||
kwargs = {
|
||||
'encoding': encoding or UTF8,
|
||||
'indent': ' ' * indent,
|
||||
}
|
||||
if standalone is not None and sys.version_info >= (3, 9):
|
||||
kwargs['standalone'] = standalone
|
||||
body = document.toprettyxml(**kwargs).decode()
|
||||
body = document.toprettyxml(**kwargs).decode(kwargs['encoding'])
|
||||
|
||||
# Remove blank lines automatically added by `toprettyxml()`.
|
||||
return '\n'.join(line for line in body.splitlines() if line.strip())
|
||||
lines = [line for line in body.splitlines() if line.strip()]
|
||||
|
||||
# xml.dom automatically adds the declaration, even if
|
||||
# it is not present in the actual body. Remove it.
|
||||
if len(lines) >= 1 and parse_declaration(lines[0]):
|
||||
lines.pop(0)
|
||||
if declaration:
|
||||
lines.insert(0, declaration)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class XMLFormatter(FormatterPlugin):
|
||||
@ -44,6 +63,7 @@ class XMLFormatter(FormatterPlugin):
|
||||
from xml.parsers.expat import ExpatError
|
||||
from defusedxml.common import DefusedXmlException
|
||||
|
||||
declaration = parse_declaration(body)
|
||||
try:
|
||||
parsed_body = parse_xml(body)
|
||||
except ExpatError:
|
||||
@ -54,6 +74,6 @@ class XMLFormatter(FormatterPlugin):
|
||||
body = pretty_xml(parsed_body,
|
||||
encoding=parsed_body.encoding,
|
||||
indent=self.format_options['xml']['indent'],
|
||||
standalone=parsed_body.standalone)
|
||||
declaration=declaration)
|
||||
|
||||
return body
|
||||
|
0
httpie/output/lexers/__init__.py
Normal file
0
httpie/output/lexers/__init__.py
Normal file
12
httpie/output/lexers/common.py
Normal file
12
httpie/output/lexers/common.py
Normal file
@ -0,0 +1,12 @@
|
||||
def precise(lexer, precise_token, parent_token):
|
||||
# Due to a pygments bug*, custom tokens will look bad
|
||||
# on outside styles. Until it is fixed on upstream, we'll
|
||||
# convey whether the client is using pie style or not
|
||||
# through precise option and return more precise tokens
|
||||
# depending on it's value.
|
||||
#
|
||||
# [0]: https://github.com/pygments/pygments/issues/1986
|
||||
if precise_token is None or not lexer.options.get("precise"):
|
||||
return parent_token
|
||||
else:
|
||||
return precise_token
|
97
httpie/output/lexers/http.py
Normal file
97
httpie/output/lexers/http.py
Normal file
@ -0,0 +1,97 @@
|
||||
import re
|
||||
import pygments
|
||||
from httpie.output.lexers.common import precise
|
||||
|
||||
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
|
||||
|
||||
STATUS_TYPES = {
|
||||
'1': pygments.token.Number.HTTP.INFO,
|
||||
'2': pygments.token.Number.HTTP.OK,
|
||||
'3': pygments.token.Number.HTTP.REDIRECT,
|
||||
'4': pygments.token.Number.HTTP.CLIENT_ERR,
|
||||
'5': pygments.token.Number.HTTP.SERVER_ERR,
|
||||
}
|
||||
|
||||
RESPONSE_TYPES = {
|
||||
'GET': pygments.token.Name.Function.HTTP.GET,
|
||||
'HEAD': pygments.token.Name.Function.HTTP.HEAD,
|
||||
'POST': pygments.token.Name.Function.HTTP.POST,
|
||||
'PUT': pygments.token.Name.Function.HTTP.PUT,
|
||||
'PATCH': pygments.token.Name.Function.HTTP.PATCH,
|
||||
'DELETE': pygments.token.Name.Function.HTTP.DELETE,
|
||||
}
|
||||
|
||||
|
||||
def http_response_type(lexer, match, ctx):
|
||||
status_match = RE_STATUS_LINE.match(match.group())
|
||||
if status_match is None:
|
||||
return None
|
||||
|
||||
status_code, text, reason = status_match.groups()
|
||||
status_type = precise(
|
||||
lexer,
|
||||
STATUS_TYPES.get(status_code[0]),
|
||||
pygments.token.Number
|
||||
)
|
||||
|
||||
groups = pygments.lexer.bygroups(
|
||||
status_type,
|
||||
pygments.token.Text,
|
||||
status_type
|
||||
)
|
||||
yield from groups(lexer, status_match, ctx)
|
||||
|
||||
|
||||
def request_method(lexer, match, ctx):
|
||||
response_type = precise(
|
||||
lexer,
|
||||
RESPONSE_TYPES.get(match.group()),
|
||||
pygments.token.Name.Function
|
||||
)
|
||||
yield match.start(), response_type, match.group()
|
||||
|
||||
|
||||
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
"""Simplified HTTP lexer for Pygments.
|
||||
|
||||
It only operates on headers and provides a stronger contrast between
|
||||
their names and values than the original one bundled with Pygments
|
||||
(:class:`pygments.lexers.text import HttpLexer`), especially when
|
||||
Solarized color scheme is used.
|
||||
|
||||
"""
|
||||
name = 'HTTP'
|
||||
aliases = ['http']
|
||||
filenames = ['*.http']
|
||||
tokens = {
|
||||
'root': [
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
pygments.lexer.bygroups(
|
||||
request_method,
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Namespace,
|
||||
pygments.token.Text,
|
||||
pygments.token.Keyword.Reserved,
|
||||
pygments.token.Operator,
|
||||
pygments.token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(.+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Keyword.Reserved, # 'HTTP'
|
||||
pygments.token.Operator, # '/'
|
||||
pygments.token.Number, # Version
|
||||
pygments.token.Text,
|
||||
http_response_type, # Status code and Reason
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Attribute, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
pygments.token.String # Value
|
||||
))
|
||||
]
|
||||
}
|
31
httpie/output/lexers/json.py
Normal file
31
httpie/output/lexers/json.py
Normal file
@ -0,0 +1,31 @@
|
||||
import re
|
||||
|
||||
from pygments.lexer import bygroups, using, RegexLexer
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from pygments.token import Token
|
||||
|
||||
PREFIX_TOKEN = Token.Error
|
||||
PREFIX_REGEX = r'[^{\["]+'
|
||||
|
||||
|
||||
class EnhancedJsonLexer(RegexLexer):
|
||||
"""
|
||||
Enhanced JSON lexer for Pygments.
|
||||
|
||||
It adds support for eventual data prefixing the actual JSON body.
|
||||
|
||||
"""
|
||||
name = 'JSON'
|
||||
flags = re.IGNORECASE | re.DOTALL
|
||||
tokens = {
|
||||
'root': [
|
||||
# Eventual non-JSON data prefix followed by actual JSON body.
|
||||
# FIX: data prefix + number (integer or float) is not correctly handled.
|
||||
(
|
||||
fr'({PREFIX_REGEX})' + r'((?:[{\["]|true|false|null).+)',
|
||||
bygroups(PREFIX_TOKEN, using(JsonLexer))
|
||||
),
|
||||
# JSON body.
|
||||
(r'.+', using(JsonLexer)),
|
||||
],
|
||||
}
|
57
httpie/output/lexers/metadata.py
Normal file
57
httpie/output/lexers/metadata.py
Normal file
@ -0,0 +1,57 @@
|
||||
import pygments
|
||||
from httpie.output.lexers.common import precise
|
||||
|
||||
SPEED_TOKENS = {
|
||||
0.45: pygments.token.Number.SPEED.FAST,
|
||||
1.00: pygments.token.Number.SPEED.AVG,
|
||||
2.50: pygments.token.Number.SPEED.SLOW,
|
||||
}
|
||||
|
||||
|
||||
def speed_based_token(lexer, match, ctx):
|
||||
try:
|
||||
value = float(match.group())
|
||||
except ValueError:
|
||||
return pygments.token.Number
|
||||
|
||||
for limit, token in SPEED_TOKENS.items():
|
||||
if value <= limit:
|
||||
break
|
||||
else:
|
||||
token = pygments.token.Number.SPEED.VERY_SLOW
|
||||
|
||||
response_type = precise(
|
||||
lexer,
|
||||
token,
|
||||
pygments.token.Number
|
||||
)
|
||||
yield match.start(), response_type, match.group()
|
||||
|
||||
|
||||
class MetadataLexer(pygments.lexer.RegexLexer):
|
||||
"""Simple HTTPie metadata lexer."""
|
||||
|
||||
tokens = {
|
||||
'root': [
|
||||
(
|
||||
r'(Elapsed time)( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Decorator, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
speed_based_token,
|
||||
pygments.token.Name.Builtin # Value
|
||||
)
|
||||
),
|
||||
# Generic item
|
||||
(
|
||||
r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Decorator, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
pygments.token.Text # Value
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
@ -51,3 +51,8 @@ class Formatting:
|
||||
for p in self.enabled_plugins:
|
||||
content = p.format_body(content, mime)
|
||||
return content
|
||||
|
||||
def format_metadata(self, metadata: str) -> str:
|
||||
for p in self.enabled_plugins:
|
||||
metadata = p.format_metadata(metadata)
|
||||
return metadata
|
||||
|
@ -1,11 +1,12 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from itertools import chain
|
||||
from typing import Callable, Iterable, Union
|
||||
from typing import Callable, Iterable, Optional, Union
|
||||
|
||||
from ..context import Environment
|
||||
from ..constants import UTF8
|
||||
from ..models import HTTPMessage
|
||||
from .processing import Conversion, Formatting
|
||||
from ..context import Environment
|
||||
from ..encoding import smart_decode, smart_encode, UTF8
|
||||
from ..models import HTTPMessage, OutputOptions
|
||||
from ..utils import parse_content_type_header
|
||||
|
||||
|
||||
BINARY_SUPPRESSED_NOTICE = (
|
||||
@ -32,47 +33,55 @@ class BaseStream(metaclass=ABCMeta):
|
||||
def __init__(
|
||||
self,
|
||||
msg: HTTPMessage,
|
||||
with_headers=True,
|
||||
with_body=True,
|
||||
output_options: OutputOptions,
|
||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
||||
):
|
||||
"""
|
||||
:param msg: a :class:`models.HTTPMessage` subclass
|
||||
:param with_headers: if `True`, headers will be included
|
||||
:param with_body: if `True`, body will be included
|
||||
|
||||
:param output_options: a :class:`OutputOptions` instance to represent
|
||||
which parts of the message is printed.
|
||||
"""
|
||||
assert with_headers or with_body
|
||||
assert output_options.any()
|
||||
self.msg = msg
|
||||
self.with_headers = with_headers
|
||||
self.with_body = with_body
|
||||
self.output_options = output_options
|
||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||
|
||||
def get_headers(self) -> bytes:
|
||||
"""Return the headers' bytes."""
|
||||
return self.msg.headers.encode()
|
||||
|
||||
def get_metadata(self) -> bytes:
|
||||
"""Return the message metadata."""
|
||||
return self.msg.metadata.encode()
|
||||
|
||||
@abstractmethod
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
"""Return an iterator over the message body."""
|
||||
|
||||
def __iter__(self) -> Iterable[bytes]:
|
||||
"""Return an iterator over `self.msg`."""
|
||||
if self.with_headers:
|
||||
if self.output_options.headers:
|
||||
yield self.get_headers()
|
||||
yield b'\r\n\r\n'
|
||||
|
||||
if self.with_body:
|
||||
if self.output_options.body:
|
||||
try:
|
||||
for chunk in self.iter_body():
|
||||
yield chunk
|
||||
if self.on_body_chunk_downloaded:
|
||||
self.on_body_chunk_downloaded(chunk)
|
||||
except DataSuppressedError as e:
|
||||
if self.with_headers:
|
||||
if self.output_options.headers:
|
||||
yield b'\n'
|
||||
yield e.message
|
||||
|
||||
if self.output_options.meta:
|
||||
if self.output_options.body:
|
||||
yield b'\n\n'
|
||||
|
||||
yield self.get_metadata()
|
||||
yield b'\n\n'
|
||||
|
||||
|
||||
class RawStream(BaseStream):
|
||||
"""The message is streamed in chunks with no processing."""
|
||||
@ -88,6 +97,9 @@ class RawStream(BaseStream):
|
||||
return self.msg.iter_body(self.chunk_size)
|
||||
|
||||
|
||||
ENCODING_GUESS_THRESHOLD = 3
|
||||
|
||||
|
||||
class EncodedStream(BaseStream):
|
||||
"""Encoded HTTP message stream.
|
||||
|
||||
@ -98,8 +110,20 @@ class EncodedStream(BaseStream):
|
||||
"""
|
||||
CHUNK_SIZE = 1
|
||||
|
||||
def __init__(self, env=Environment(), **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
env=Environment(),
|
||||
mime_overwrite: str = None,
|
||||
encoding_overwrite: str = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
if mime_overwrite:
|
||||
self.mime = mime_overwrite
|
||||
else:
|
||||
self.mime, _ = parse_content_type_header(self.msg.content_type)
|
||||
self._encoding = encoding_overwrite or self.msg.encoding
|
||||
self._encoding_guesses = []
|
||||
if env.stdout_isatty:
|
||||
# Use the encoding supported by the terminal.
|
||||
output_encoding = env.stdout_encoding
|
||||
@ -113,8 +137,32 @@ class EncodedStream(BaseStream):
|
||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||
if b'\0' in line:
|
||||
raise BinarySuppressedError()
|
||||
yield line.decode(self.msg.encoding) \
|
||||
.encode(self.output_encoding, 'replace') + lf
|
||||
line = self.decode_chunk(line)
|
||||
yield smart_encode(line, self.output_encoding) + lf
|
||||
|
||||
def decode_chunk(self, raw_chunk: str) -> str:
|
||||
chunk, guessed_encoding = smart_decode(raw_chunk, self.encoding)
|
||||
self._encoding_guesses.append(guessed_encoding)
|
||||
return chunk
|
||||
|
||||
@property
|
||||
def encoding(self) -> Optional[str]:
|
||||
if self._encoding:
|
||||
return self._encoding
|
||||
|
||||
# If we find a reliable (used consecutively) encoding, than
|
||||
# use it for the next iterations.
|
||||
if len(self._encoding_guesses) < ENCODING_GUESS_THRESHOLD:
|
||||
return None
|
||||
|
||||
guess_1, guess_2 = self._encoding_guesses[-2:]
|
||||
if guess_1 == guess_2:
|
||||
self._encoding = guess_1
|
||||
return guess_1
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, value) -> None:
|
||||
self._encoding = value
|
||||
|
||||
|
||||
class PrettyStream(EncodedStream):
|
||||
@ -136,12 +184,15 @@ class PrettyStream(EncodedStream):
|
||||
super().__init__(**kwargs)
|
||||
self.formatting = formatting
|
||||
self.conversion = conversion
|
||||
self.mime = self.msg.content_type.split(';')[0]
|
||||
|
||||
def get_headers(self) -> bytes:
|
||||
return self.formatting.format_headers(
|
||||
self.msg.headers).encode(self.output_encoding)
|
||||
|
||||
def get_metadata(self) -> bytes:
|
||||
return self.formatting.format_metadata(
|
||||
self.msg.metadata).encode(self.output_encoding)
|
||||
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
first_chunk = True
|
||||
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
||||
@ -167,9 +218,9 @@ class PrettyStream(EncodedStream):
|
||||
if not isinstance(chunk, str):
|
||||
# Text when a converter has been used,
|
||||
# otherwise it will always be bytes.
|
||||
chunk = chunk.decode(self.msg.encoding, 'replace')
|
||||
chunk = self.decode_chunk(chunk)
|
||||
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
|
||||
return chunk.encode(self.output_encoding, 'replace')
|
||||
return smart_encode(chunk, self.output_encoding)
|
||||
|
||||
|
||||
class BufferedPrettyStream(PrettyStream):
|
||||
|
0
httpie/output/ui/__init__.py
Normal file
0
httpie/output/ui/__init__.py
Normal file
161
httpie/output/ui/palette.py
Normal file
161
httpie/output/ui/palette.py
Normal file
@ -0,0 +1,161 @@
|
||||
# Copy the brand palette
|
||||
from typing import Optional
|
||||
|
||||
COLOR_PALETTE = {
|
||||
'transparent': 'transparent',
|
||||
'current': 'currentColor',
|
||||
'white': '#F5F5F0',
|
||||
'black': '#1C1818',
|
||||
'grey': {
|
||||
'50': '#F5F5F0',
|
||||
'100': '#EDEDEB',
|
||||
'200': '#D1D1CF',
|
||||
'300': '#B5B5B2',
|
||||
'400': '#999999',
|
||||
'500': '#7D7D7D',
|
||||
'600': '#666663',
|
||||
'700': '#4F4D4D',
|
||||
'800': '#363636',
|
||||
'900': '#1C1818',
|
||||
'DEFAULT': '#7D7D7D',
|
||||
},
|
||||
'aqua': {
|
||||
'50': '#E8F0F5',
|
||||
'100': '#D6E3ED',
|
||||
'200': '#C4D9E5',
|
||||
'300': '#B0CCDE',
|
||||
'400': '#9EBFD6',
|
||||
'500': '#8CB4CD',
|
||||
'600': '#7A9EB5',
|
||||
'700': '#698799',
|
||||
'800': '#597082',
|
||||
'900': '#455966',
|
||||
'DEFAULT': '#8CB4CD',
|
||||
},
|
||||
'purple': {
|
||||
'50': '#F0E0FC',
|
||||
'100': '#E3C7FA',
|
||||
'200': '#D9ADF7',
|
||||
'300': '#CC96F5',
|
||||
'400': '#BF7DF2',
|
||||
'500': '#B464F0',
|
||||
'600': '#9E54D6',
|
||||
'700': '#8745BA',
|
||||
'800': '#70389E',
|
||||
'900': '#5C2982',
|
||||
'DEFAULT': '#B464F0',
|
||||
},
|
||||
'orange': {
|
||||
'50': '#FFEDDB',
|
||||
'100': '#FFDEBF',
|
||||
'200': '#FFCFA3',
|
||||
'300': '#FFBF87',
|
||||
'400': '#FFB06B',
|
||||
'500': '#FFA24E',
|
||||
'600': '#F2913D',
|
||||
'700': '#E3822B',
|
||||
'800': '#D6701C',
|
||||
'900': '#C75E0A',
|
||||
'DEFAULT': '#FFA24E',
|
||||
},
|
||||
'red': {
|
||||
'50': '#FFE0DE',
|
||||
'100': '#FFC7C4',
|
||||
'200': '#FFB0AB',
|
||||
'300': '#FF968F',
|
||||
'400': '#FF8075',
|
||||
'500': '#FF665B',
|
||||
'600': '#E34F45',
|
||||
'700': '#C7382E',
|
||||
'800': '#AD2117',
|
||||
'900': '#910A00',
|
||||
'DEFAULT': '#FF665B',
|
||||
},
|
||||
'blue': {
|
||||
'50': '#DBE3FA',
|
||||
'100': '#BFCFF5',
|
||||
'200': '#A1B8F2',
|
||||
'300': '#85A3ED',
|
||||
'400': '#698FEB',
|
||||
'500': '#4B78E6',
|
||||
'600': '#426BD1',
|
||||
'700': '#3B5EBA',
|
||||
'800': '#3354A6',
|
||||
'900': '#2B478F',
|
||||
'DEFAULT': '#4B78E6',
|
||||
},
|
||||
'pink': {
|
||||
'50': '#FFEBFF',
|
||||
'100': '#FCDBFC',
|
||||
'200': '#FCCCFC',
|
||||
'300': '#FCBAFC',
|
||||
'400': '#FAABFA',
|
||||
'500': '#FA9BFA',
|
||||
'600': '#DE85DE',
|
||||
'700': '#C26EC2',
|
||||
'800': '#A854A6',
|
||||
'900': '#8C3D8A',
|
||||
'DEFAULT': '#FA9BFA',
|
||||
},
|
||||
'green': {
|
||||
'50': '#E3F7E8',
|
||||
'100': '#CCF2D6',
|
||||
'200': '#B5EDC4',
|
||||
'300': '#A1E8B0',
|
||||
'400': '#8AE09E',
|
||||
'500': '#73DC8C',
|
||||
'600': '#63C27A',
|
||||
'700': '#52AB66',
|
||||
'800': '#429154',
|
||||
'900': '#307842',
|
||||
'DEFAULT': '#73DC8C',
|
||||
},
|
||||
'yellow': {
|
||||
'50': '#F7F7DB',
|
||||
'100': '#F2F2BF',
|
||||
'200': '#EDEDA6',
|
||||
'300': '#E5E88A',
|
||||
'400': '#E0E36E',
|
||||
'500': '#DBDE52',
|
||||
'600': '#CCCC3D',
|
||||
'700': '#BABA29',
|
||||
'800': '#ABA614',
|
||||
'900': '#999400',
|
||||
'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))
|
||||
]
|
||||
|
||||
|
||||
def get_color(color: str, shade: str) -> Optional[str]:
|
||||
if color not in COLOR_PALETTE:
|
||||
return None
|
||||
|
||||
color_code = COLOR_PALETTE[color]
|
||||
if isinstance(color_code, dict) and shade in color_code:
|
||||
return color_code[shade]
|
||||
else:
|
||||
return color_code
|
37
httpie/output/utils.py
Normal file
37
httpie/output/utils.py
Normal file
@ -0,0 +1,37 @@
|
||||
import json
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from ..utils import load_json_preserve_order_and_dupe_keys
|
||||
from .lexers.json import PREFIX_REGEX
|
||||
|
||||
|
||||
def load_prefixed_json(data: str) -> Tuple[str, json.JSONDecoder]:
|
||||
"""Simple JSON loading from `data`.
|
||||
|
||||
"""
|
||||
# First, the full data.
|
||||
try:
|
||||
return '', load_json_preserve_order_and_dupe_keys(data)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Then, try to find the start of the actual body.
|
||||
data_prefix, body = parse_prefixed_json(data)
|
||||
try:
|
||||
return data_prefix, load_json_preserve_order_and_dupe_keys(body)
|
||||
except ValueError:
|
||||
raise ValueError('Invalid JSON')
|
||||
|
||||
|
||||
def parse_prefixed_json(data: str) -> Tuple[str, str]:
|
||||
"""Find the potential JSON body from `data`.
|
||||
|
||||
Sometimes the JSON body is prefixed with a XSSI magic string, specific to the server.
|
||||
Return a tuple (data prefix, actual JSON body).
|
||||
|
||||
"""
|
||||
matches = re.findall(PREFIX_REGEX, data)
|
||||
data_prefix = matches[0] if matches else ''
|
||||
body = data[len(data_prefix):]
|
||||
return data_prefix, body
|
@ -2,10 +2,16 @@ import argparse
|
||||
import errno
|
||||
from typing import IO, TextIO, Tuple, Type, Union
|
||||
|
||||
import requests
|
||||
|
||||
from ..cli.dicts import HTTPHeadersDict
|
||||
from ..context import Environment
|
||||
from ..models import HTTPRequest, HTTPResponse
|
||||
from ..models import (
|
||||
HTTPRequest,
|
||||
HTTPResponse,
|
||||
HTTPMessage,
|
||||
RequestsMessage,
|
||||
RequestsMessageKind,
|
||||
OutputOptions
|
||||
)
|
||||
from .processing import Conversion, Formatting
|
||||
from .streams import (
|
||||
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
||||
@ -17,21 +23,19 @@ MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
|
||||
|
||||
|
||||
def write_message(
|
||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
||||
requests_message: RequestsMessage,
|
||||
env: Environment,
|
||||
args: argparse.Namespace,
|
||||
with_headers=False,
|
||||
with_body=False,
|
||||
output_options: OutputOptions,
|
||||
):
|
||||
if not (with_body or with_headers):
|
||||
if not output_options.any():
|
||||
return
|
||||
write_stream_kwargs = {
|
||||
'stream': build_output_stream_for_message(
|
||||
args=args,
|
||||
env=env,
|
||||
requests_message=requests_message,
|
||||
with_body=with_body,
|
||||
with_headers=with_headers,
|
||||
output_options=output_options,
|
||||
),
|
||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||
'outfile': env.stdout,
|
||||
@ -93,25 +97,25 @@ def write_stream_with_colors_win(
|
||||
def build_output_stream_for_message(
|
||||
args: argparse.Namespace,
|
||||
env: Environment,
|
||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
||||
with_headers: bool,
|
||||
with_body: bool,
|
||||
requests_message: RequestsMessage,
|
||||
output_options: OutputOptions,
|
||||
):
|
||||
message_type = {
|
||||
RequestsMessageKind.REQUEST: HTTPRequest,
|
||||
RequestsMessageKind.RESPONSE: HTTPResponse,
|
||||
}[output_options.kind]
|
||||
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
||||
env=env,
|
||||
args=args,
|
||||
message_type=message_type,
|
||||
headers=requests_message.headers
|
||||
)
|
||||
message_class = {
|
||||
requests.PreparedRequest: HTTPRequest,
|
||||
requests.Response: HTTPResponse,
|
||||
}[type(requests_message)]
|
||||
yield from stream_class(
|
||||
msg=message_class(requests_message),
|
||||
with_headers=with_headers,
|
||||
with_body=with_body,
|
||||
msg=message_type(requests_message),
|
||||
output_options=output_options,
|
||||
**stream_kwargs,
|
||||
)
|
||||
if (env.stdout_isatty and with_body
|
||||
if (env.stdout_isatty and output_options.body and not output_options.meta
|
||||
and not getattr(requests_message, 'is_body_upload_chunk', False)):
|
||||
# Ensure a blank line after the response body.
|
||||
# For terminal output only.
|
||||
@ -120,37 +124,49 @@ def build_output_stream_for_message(
|
||||
|
||||
def get_stream_type_and_kwargs(
|
||||
env: Environment,
|
||||
args: argparse.Namespace
|
||||
args: argparse.Namespace,
|
||||
message_type: Type[HTTPMessage],
|
||||
headers: HTTPHeadersDict,
|
||||
) -> Tuple[Type['BaseStream'], dict]:
|
||||
"""Pick the right stream type and kwargs for it based on `env` and `args`.
|
||||
|
||||
"""
|
||||
is_stream = args.stream
|
||||
if not is_stream and message_type is HTTPResponse:
|
||||
# If this is a response, then check the headers for determining
|
||||
# auto-streaming.
|
||||
is_stream = headers.get('Content-Type') == 'text/event-stream'
|
||||
|
||||
if not env.stdout_isatty and not args.prettify:
|
||||
stream_class = RawStream
|
||||
stream_kwargs = {
|
||||
'chunk_size': (
|
||||
RawStream.CHUNK_SIZE_BY_LINE
|
||||
if args.stream
|
||||
if is_stream
|
||||
else RawStream.CHUNK_SIZE
|
||||
)
|
||||
}
|
||||
elif args.prettify:
|
||||
stream_class = PrettyStream if args.stream else BufferedPrettyStream
|
||||
stream_kwargs = {
|
||||
'env': env,
|
||||
'conversion': Conversion(),
|
||||
'formatting': Formatting(
|
||||
env=env,
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
format_options=args.format_options,
|
||||
)
|
||||
}
|
||||
else:
|
||||
stream_class = EncodedStream
|
||||
stream_kwargs = {
|
||||
'env': env
|
||||
'env': env,
|
||||
}
|
||||
if message_type is HTTPResponse:
|
||||
stream_kwargs.update({
|
||||
'mime_overwrite': args.response_mime,
|
||||
'encoding_overwrite': args.response_charset,
|
||||
})
|
||||
if args.prettify:
|
||||
stream_class = PrettyStream if is_stream else BufferedPrettyStream
|
||||
stream_kwargs.update({
|
||||
'conversion': Conversion(),
|
||||
'formatting': Formatting(
|
||||
env=env,
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
format_options=args.format_options,
|
||||
)
|
||||
})
|
||||
|
||||
return stream_class, stream_kwargs
|
||||
|
@ -1,5 +1,7 @@
|
||||
class BasePlugin:
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
# The name of the plugin, eg. "My auth".
|
||||
name = None
|
||||
|
||||
@ -53,7 +55,7 @@ class AuthPlugin(BasePlugin):
|
||||
# then this is `None`.
|
||||
raw_auth = None
|
||||
|
||||
def get_auth(self, username=None, password=None):
|
||||
def get_auth(self, username: str = None, password: str = None):
|
||||
"""
|
||||
If `auth_parse` is set to `True`, then `username`
|
||||
and `password` contain the parsed credentials.
|
||||
@ -93,7 +95,7 @@ class TransportPlugin(BasePlugin):
|
||||
|
||||
class ConverterPlugin(BasePlugin):
|
||||
"""
|
||||
Possibly converts response data for prettified terminal display.
|
||||
Possibly converts binary response data for prettified terminal display.
|
||||
|
||||
See httpie-msgpack for an example converter plugin:
|
||||
|
||||
@ -101,14 +103,21 @@ class ConverterPlugin(BasePlugin):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, mime):
|
||||
def __init__(self, mime: str):
|
||||
self.mime = mime
|
||||
|
||||
def convert(self, content_bytes):
|
||||
def convert(self, body: bytes) -> Tuple[str, str]:
|
||||
"""
|
||||
Convert a binary body to a textual representation for the terminal
|
||||
and return a tuple containing the new Content-Type and content, e.g.:
|
||||
|
||||
('application/json', '{}')
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def supports(cls, mime):
|
||||
def supports(cls, mime: str) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@ -146,3 +155,11 @@ class FormatterPlugin(BasePlugin):
|
||||
|
||||
"""
|
||||
return content
|
||||
|
||||
def format_metadata(self, metadata: str) -> str:
|
||||
"""Return processed `metadata`.
|
||||
|
||||
:param metadata: The metadata as text.
|
||||
|
||||
"""
|
||||
return metadata
|
||||
|
@ -34,6 +34,16 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
|
||||
return f'Basic {token}'
|
||||
|
||||
|
||||
class HTTPBearerAuth(requests.auth.AuthBase):
|
||||
|
||||
def __init__(self, token: str) -> None:
|
||||
self.token = token
|
||||
|
||||
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
|
||||
request.headers['Authorization'] = f'Bearer {self.token}'
|
||||
return request
|
||||
|
||||
|
||||
class BasicAuthPlugin(BuiltinAuthPlugin):
|
||||
name = 'Basic HTTP auth'
|
||||
auth_type = 'basic'
|
||||
@ -56,3 +66,14 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
|
||||
password: str
|
||||
) -> requests.auth.HTTPDigestAuth:
|
||||
return requests.auth.HTTPDigestAuth(username, password)
|
||||
|
||||
|
||||
class BearerAuthPlugin(BuiltinAuthPlugin):
|
||||
name = 'Bearer HTTP Auth'
|
||||
auth_type = 'bearer'
|
||||
netrc_parse = False
|
||||
auth_parse = False
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def get_auth(self, **kwargs) -> requests.auth.HTTPDigestAuth:
|
||||
return HTTPBearerAuth(self.raw_auth)
|
||||
|
@ -1,23 +1,47 @@
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
from typing import Dict, List, Type
|
||||
from typing import Dict, List, Type, Iterator, Optional, ContextManager
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager, nullcontext
|
||||
|
||||
from pkg_resources import iter_entry_points
|
||||
from ..compat import importlib_metadata, find_entry_points, get_dist_name
|
||||
|
||||
from . import AuthPlugin, ConverterPlugin, FormatterPlugin
|
||||
from .base import BasePlugin, TransportPlugin
|
||||
from ..utils import repr_dict, as_site
|
||||
from . import AuthPlugin, ConverterPlugin, FormatterPlugin, TransportPlugin
|
||||
from .base import BasePlugin
|
||||
|
||||
|
||||
ENTRY_POINT_NAMES = [
|
||||
'httpie.plugins.auth.v1',
|
||||
'httpie.plugins.formatter.v1',
|
||||
'httpie.plugins.converter.v1',
|
||||
'httpie.plugins.transport.v1',
|
||||
]
|
||||
ENTRY_POINT_CLASSES = {
|
||||
'httpie.plugins.auth.v1': AuthPlugin,
|
||||
'httpie.plugins.converter.v1': ConverterPlugin,
|
||||
'httpie.plugins.formatter.v1': FormatterPlugin,
|
||||
'httpie.plugins.transport.v1': TransportPlugin
|
||||
}
|
||||
ENTRY_POINT_NAMES = list(ENTRY_POINT_CLASSES.keys())
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _load_directory(plugins_dir: Path) -> Iterator[None]:
|
||||
plugins_path = os.fspath(plugins_dir)
|
||||
sys.path.insert(0, plugins_path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.path.remove(plugins_path)
|
||||
|
||||
|
||||
def enable_plugins(plugins_dir: Optional[Path]) -> ContextManager[None]:
|
||||
if plugins_dir is None:
|
||||
return nullcontext()
|
||||
else:
|
||||
return _load_directory(as_site(plugins_dir))
|
||||
|
||||
|
||||
class PluginManager(list):
|
||||
|
||||
def register(self, *plugins: Type[BasePlugin]):
|
||||
for plugin in plugins:
|
||||
self.append(plugin)
|
||||
@ -28,12 +52,28 @@ class PluginManager(list):
|
||||
def filter(self, by_type=Type[BasePlugin]):
|
||||
return [plugin for plugin in self if issubclass(plugin, by_type)]
|
||||
|
||||
def load_installed_plugins(self):
|
||||
for entry_point_name in ENTRY_POINT_NAMES:
|
||||
for entry_point in iter_entry_points(entry_point_name):
|
||||
def iter_entry_points(self, directory: Optional[Path] = None):
|
||||
with enable_plugins(directory):
|
||||
eps = importlib_metadata.entry_points()
|
||||
|
||||
for entry_point_name in ENTRY_POINT_NAMES:
|
||||
yield from find_entry_points(eps, group=entry_point_name)
|
||||
|
||||
def load_installed_plugins(self, directory: Optional[Path] = None):
|
||||
for entry_point in self.iter_entry_points(directory):
|
||||
plugin_name = get_dist_name(entry_point)
|
||||
try:
|
||||
plugin = entry_point.load()
|
||||
plugin.package_name = entry_point.dist.key
|
||||
self.register(entry_point.load())
|
||||
except BaseException as exc:
|
||||
warnings.warn(
|
||||
f'While loading "{plugin_name}", an error ocurred: {exc}\n'
|
||||
f'For uninstallations, please use either "httpie plugins uninstall {plugin_name}" '
|
||||
f'or "pip uninstall {plugin_name}" (depending on how you installed it in the first '
|
||||
'place).'
|
||||
)
|
||||
continue
|
||||
plugin.package_name = plugin_name
|
||||
self.register(plugin)
|
||||
|
||||
# Auth
|
||||
def get_auth_plugins(self) -> List[Type[AuthPlugin]]:
|
||||
@ -65,5 +105,13 @@ class PluginManager(list):
|
||||
def get_transport_plugins(self) -> List[Type[TransportPlugin]]:
|
||||
return self.filter(TransportPlugin)
|
||||
|
||||
def __str__(self):
|
||||
return repr_dict({
|
||||
'adapters': self.get_transport_plugins(),
|
||||
'auth': self.get_auth_plugins(),
|
||||
'converters': self.get_converters(),
|
||||
'formatters': self.get_formatters(),
|
||||
})
|
||||
|
||||
def __repr__(self):
|
||||
return f'<PluginManager: {list(self)}>'
|
||||
return f'<{type(self).__name__} {self}>'
|
||||
|
@ -1,5 +1,5 @@
|
||||
from .manager import PluginManager
|
||||
from .builtin import BasicAuthPlugin, DigestAuthPlugin
|
||||
from .builtin import BasicAuthPlugin, DigestAuthPlugin, BearerAuthPlugin
|
||||
from ..output.formatters.headers import HeadersFormatter
|
||||
from ..output.formatters.json import JSONFormatter
|
||||
from ..output.formatters.xml import XMLFormatter
|
||||
@ -13,6 +13,7 @@ plugin_manager = PluginManager()
|
||||
plugin_manager.register(
|
||||
BasicAuthPlugin,
|
||||
DigestAuthPlugin,
|
||||
BearerAuthPlugin,
|
||||
HeadersFormatter,
|
||||
JSONFormatter,
|
||||
XMLFormatter,
|
||||
|
@ -13,7 +13,7 @@ from urllib.parse import urlsplit
|
||||
from requests.auth import AuthBase
|
||||
from requests.cookies import RequestsCookieJar, create_cookie
|
||||
|
||||
from .cli.dicts import RequestHeadersDict
|
||||
from .cli.dicts import HTTPHeadersDict
|
||||
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||
from .plugins.registry import plugin_manager
|
||||
|
||||
@ -52,7 +52,7 @@ def get_httpie_session(
|
||||
|
||||
|
||||
class Session(BaseConfigDict):
|
||||
helpurl = 'https://httpie.org/doc#sessions'
|
||||
helpurl = 'https://httpie.io/docs#sessions'
|
||||
about = 'HTTPie session file'
|
||||
|
||||
def __init__(self, path: Union[str, Path]):
|
||||
@ -65,7 +65,7 @@ class Session(BaseConfigDict):
|
||||
'password': None
|
||||
}
|
||||
|
||||
def update_headers(self, request_headers: RequestHeadersDict):
|
||||
def update_headers(self, request_headers: HTTPHeadersDict):
|
||||
"""
|
||||
Update the session headers with the request ones while ignoring
|
||||
certain name prefixes.
|
||||
@ -98,8 +98,8 @@ class Session(BaseConfigDict):
|
||||
self['headers'] = dict(headers)
|
||||
|
||||
@property
|
||||
def headers(self) -> RequestHeadersDict:
|
||||
return RequestHeadersDict(self['headers'])
|
||||
def headers(self) -> HTTPHeadersDict:
|
||||
return HTTPHeadersDict(self['headers'])
|
||||
|
||||
@property
|
||||
def cookies(self) -> RequestsCookieJar:
|
||||
@ -112,7 +112,7 @@ class Session(BaseConfigDict):
|
||||
|
||||
@cookies.setter
|
||||
def cookies(self, jar: RequestsCookieJar):
|
||||
# <https://docs.python.org/2/library/cookielib.html#cookie-objects>
|
||||
# <https://docs.python.org/3/library/cookielib.html#cookie-objects>
|
||||
stored_attrs = ['value', 'path', 'secure', 'expires']
|
||||
self['cookies'] = {}
|
||||
for cookie in jar:
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ssl
|
||||
|
||||
from requests.adapters import HTTPAdapter
|
||||
from httpie.adapters import HTTPAdapter
|
||||
# noinspection PyPackageRequirements
|
||||
from urllib3.util.ssl_ import (
|
||||
DEFAULT_CIPHERS, create_urllib3_context,
|
@ -1,15 +1,27 @@
|
||||
import sys
|
||||
import os
|
||||
import zlib
|
||||
from typing import Callable, IO, Iterable, Tuple, Union
|
||||
import functools
|
||||
from typing import Any, Callable, IO, Iterable, Optional, Tuple, Union, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from requests.utils import super_len
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
|
||||
from .context import Environment
|
||||
from .cli.dicts import MultipartRequestDataDict, RequestDataDict
|
||||
from .compat import is_windows
|
||||
|
||||
|
||||
class ChunkedUploadStream:
|
||||
class ChunkedStream:
|
||||
def __iter__(self) -> Iterable[Union[str, bytes]]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ChunkedUploadStream(ChunkedStream):
|
||||
def __init__(self, stream: Iterable, callback: Callable):
|
||||
self.callback = callback
|
||||
self.stream = stream
|
||||
@ -20,10 +32,10 @@ class ChunkedUploadStream:
|
||||
yield chunk
|
||||
|
||||
|
||||
class ChunkedMultipartUploadStream:
|
||||
class ChunkedMultipartUploadStream(ChunkedStream):
|
||||
chunk_size = 100 * 1024
|
||||
|
||||
def __init__(self, encoder: MultipartEncoder):
|
||||
def __init__(self, encoder: 'MultipartEncoder'):
|
||||
self.encoder = encoder
|
||||
|
||||
def __iter__(self) -> Iterable[Union[str, bytes]]:
|
||||
@ -34,75 +46,157 @@ class ChunkedMultipartUploadStream:
|
||||
yield chunk
|
||||
|
||||
|
||||
def as_bytes(data: Union[str, bytes]) -> bytes:
|
||||
if isinstance(data, str):
|
||||
return data.encode()
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
CallbackT = Callable[[bytes], bytes]
|
||||
|
||||
|
||||
def _wrap_function_with_callback(
|
||||
func: Callable[..., Any],
|
||||
callback: CallbackT
|
||||
) -> Callable[..., Any]:
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
chunk = func(*args, **kwargs)
|
||||
callback(chunk)
|
||||
return chunk
|
||||
return wrapped
|
||||
|
||||
|
||||
def is_stdin(file: IO) -> bool:
|
||||
try:
|
||||
file_no = file.fileno()
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return file_no == sys.stdin.fileno()
|
||||
|
||||
|
||||
READ_THRESHOLD = float(os.getenv("HTTPIE_STDIN_READ_WARN_THRESHOLD", 10.0))
|
||||
|
||||
|
||||
def observe_stdin_for_data_thread(env: Environment, file: IO) -> None:
|
||||
# Windows unfortunately does not support select() operation
|
||||
# on regular files, like stdin in our use case.
|
||||
# https://docs.python.org/3/library/select.html#select.select
|
||||
if is_windows:
|
||||
return None
|
||||
|
||||
# If the user configures READ_THRESHOLD to be 0, then
|
||||
# disable this warning.
|
||||
if READ_THRESHOLD == 0:
|
||||
return None
|
||||
|
||||
import select
|
||||
import threading
|
||||
|
||||
def worker():
|
||||
can_read, _, _ = select.select([file], [], [], READ_THRESHOLD)
|
||||
if not can_read:
|
||||
env.stderr.write(
|
||||
f'> warning: no stdin data read in {READ_THRESHOLD}s '
|
||||
f'(perhaps you want to --ignore-stdin)\n'
|
||||
f'> See: https://httpie.io/docs/cli/best-practices\n'
|
||||
)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=worker
|
||||
)
|
||||
thread.start()
|
||||
|
||||
|
||||
def _prepare_file_for_upload(
|
||||
env: Environment,
|
||||
file: Union[IO, 'MultipartEncoder'],
|
||||
callback: CallbackT,
|
||||
chunked: bool = False,
|
||||
content_length_header_value: Optional[int] = None,
|
||||
) -> Union[bytes, IO, ChunkedStream]:
|
||||
if not super_len(file):
|
||||
if is_stdin(file):
|
||||
observe_stdin_for_data_thread(env, file)
|
||||
# Zero-length -> assume stdin.
|
||||
if content_length_header_value is None and not chunked:
|
||||
# Read the whole stdin to determine `Content-Length`.
|
||||
#
|
||||
# TODO: Instead of opt-in --chunked, consider making
|
||||
# `Transfer-Encoding: chunked` for STDIN opt-out via
|
||||
# something like --no-chunked.
|
||||
# This would be backwards-incompatible so wait until v3.0.0.
|
||||
#
|
||||
file = as_bytes(file.read())
|
||||
else:
|
||||
file.read = _wrap_function_with_callback(
|
||||
file.read,
|
||||
callback
|
||||
)
|
||||
|
||||
if chunked:
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
if isinstance(file, MultipartEncoder):
|
||||
return ChunkedMultipartUploadStream(
|
||||
encoder=file,
|
||||
)
|
||||
else:
|
||||
return ChunkedUploadStream(
|
||||
stream=file,
|
||||
callback=callback,
|
||||
)
|
||||
else:
|
||||
return file
|
||||
|
||||
|
||||
def prepare_request_body(
|
||||
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
|
||||
body_read_callback: Callable[[bytes], bytes],
|
||||
content_length_header_value: int = None,
|
||||
chunked=False,
|
||||
offline=False,
|
||||
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
|
||||
|
||||
is_file_like = hasattr(body, 'read')
|
||||
|
||||
if isinstance(body, RequestDataDict):
|
||||
body = urlencode(body, doseq=True)
|
||||
env: Environment,
|
||||
raw_body: Union[str, bytes, IO, 'MultipartEncoder', RequestDataDict],
|
||||
body_read_callback: CallbackT,
|
||||
offline: bool = False,
|
||||
chunked: bool = False,
|
||||
content_length_header_value: Optional[int] = None,
|
||||
) -> Union[bytes, IO, 'MultipartEncoder', ChunkedStream]:
|
||||
is_file_like = hasattr(raw_body, 'read')
|
||||
if isinstance(raw_body, (bytes, str)):
|
||||
body = as_bytes(raw_body)
|
||||
elif isinstance(raw_body, RequestDataDict):
|
||||
body = as_bytes(urlencode(raw_body, doseq=True))
|
||||
else:
|
||||
body = raw_body
|
||||
|
||||
if offline:
|
||||
if is_file_like:
|
||||
return body.read()
|
||||
return body
|
||||
|
||||
if not is_file_like:
|
||||
if chunked:
|
||||
body = ChunkedUploadStream(
|
||||
# Pass the entire body as one chunk.
|
||||
stream=(chunk.encode() for chunk in [body]),
|
||||
callback=body_read_callback,
|
||||
)
|
||||
else:
|
||||
# File-like object.
|
||||
|
||||
if not super_len(body):
|
||||
# Zero-length -> assume stdin.
|
||||
if content_length_header_value is None and not chunked:
|
||||
#
|
||||
# Read the whole stdin to determine `Content-Length`.
|
||||
#
|
||||
# TODO: Instead of opt-in --chunked, consider making
|
||||
# `Transfer-Encoding: chunked` for STDIN opt-out via
|
||||
# something like --no-chunked.
|
||||
# This would be backwards-incompatible so wait until v3.0.0.
|
||||
#
|
||||
body = body.read()
|
||||
return as_bytes(raw_body.read())
|
||||
else:
|
||||
orig_read = body.read
|
||||
return body
|
||||
|
||||
def new_read(*args):
|
||||
chunk = orig_read(*args)
|
||||
body_read_callback(chunk)
|
||||
return chunk
|
||||
|
||||
body.read = new_read
|
||||
|
||||
if chunked:
|
||||
if isinstance(body, MultipartEncoder):
|
||||
body = ChunkedMultipartUploadStream(
|
||||
encoder=body,
|
||||
)
|
||||
else:
|
||||
body = ChunkedUploadStream(
|
||||
stream=body,
|
||||
callback=body_read_callback,
|
||||
)
|
||||
|
||||
return body
|
||||
if is_file_like:
|
||||
return _prepare_file_for_upload(
|
||||
env,
|
||||
body,
|
||||
chunked=chunked,
|
||||
callback=body_read_callback,
|
||||
content_length_header_value=content_length_header_value
|
||||
)
|
||||
elif chunked:
|
||||
return ChunkedUploadStream(
|
||||
stream=iter([body]),
|
||||
callback=body_read_callback
|
||||
)
|
||||
else:
|
||||
return body
|
||||
|
||||
|
||||
def get_multipart_data_and_content_type(
|
||||
data: MultipartRequestDataDict,
|
||||
boundary: str = None,
|
||||
content_type: str = None,
|
||||
) -> Tuple[MultipartEncoder, str]:
|
||||
) -> Tuple['MultipartEncoder', str]:
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
|
||||
encoder = MultipartEncoder(
|
||||
fields=data.items(),
|
||||
boundary=boundary,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user