mirror of
https://github.com/httpie/cli.git
synced 2025-08-11 00:13:36 +02:00
Compare commits
127 Commits
Author | SHA1 | Date | |
---|---|---|---|
559134de0a | |||
813e8864a1 | |||
45fcd746d7 | |||
d5e3611e85 | |||
378a1f513e | |||
df6843b15a | |||
640901146f | |||
6b5d96da72 | |||
97bd9c2a89 | |||
708608e1d4 | |||
d56a1f216e | |||
738a6bea57 | |||
ec521c461b | |||
212000199e | |||
700dbeddb0 | |||
30a4d29f77 | |||
aedcad7e2a | |||
202f59e04a | |||
ba0c1ab258 | |||
217cf8ddae | |||
859e442083 | |||
4e59bbfae6 | |||
caa8fb9058 | |||
2797b7244c | |||
3b441fa57e | |||
c815e21ef9 | |||
8a03b7a824 | |||
b3f29c8d1e | |||
a88e44c284 | |||
c97fe64a37 | |||
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 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -3,7 +3,7 @@ name: Bug report
|
||||
about: Report a possible bug in HTTPie
|
||||
title: ''
|
||||
labels: "new, bug"
|
||||
assignees: 'BoboTiG'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
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
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -19,6 +19,9 @@ jobs:
|
||||
- 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
|
||||
|
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
|
@ -3,6 +3,7 @@ on:
|
||||
paths:
|
||||
- .github/workflows/test-package-linux-snap.yml
|
||||
- snapcraft.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
snap:
|
||||
@ -18,6 +19,7 @@ jobs:
|
||||
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
|
||||
|
1
.github/workflows/test-package-mac-brew.yml
vendored
1
.github/workflows/test-package-mac-brew.yml
vendored
@ -3,6 +3,7 @@ on:
|
||||
paths:
|
||||
- .github/workflows/test-package-mac-brew.yml
|
||||
- docs/packaging/brew/httpie.rb
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
brew:
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
|
||||
python-version: [3.7, 3.8, 3.9, "3.10"]
|
||||
pyopenssl: [0, 1]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
@ -3,7 +3,8 @@
|
||||
specfile_path: httpie.spec
|
||||
actions:
|
||||
# get the current Fedora Rawhide specfile:
|
||||
post-upstream-clone: "wget https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -O httpie.spec"
|
||||
# post-upstream-clone: "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
|
||||
|
41
CHANGELOG.md
41
CHANGELOG.md
@ -3,8 +3,47 @@
|
||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [3.0.2](https://github.com/httpie/httpie/compare/3.0.0...3.0.1) (2022-01-23)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Fixed usage of `httpie` when there is a presence of a config with `default_options` ([#1280](https://github.com/httpie/httpie/pull/1280)).
|
||||
|
||||
## [3.0.1](https://github.com/httpie/httpie/compare/3.0.0...3.0.1) (2022-01-23)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Changed the value shown as time elapsed from time-to-read-headers to total exchange time ([#1277](https://github.com/httpie/httpie/issues/1277))
|
||||
|
||||
## [3.0.0](https://github.com/httpie/httpie/compare/2.6.0...3.0.0) (2022-01-21)
|
||||
|
||||
[What’s new in HTTPie for Terminal 3.0 →](https://httpie.io/blog/httpie-3.0.0)
|
||||
|
||||
- Dropped support for Python 3.6. ([#1177](https://github.com/httpie/httpie/issues/1177))
|
||||
- Improved startup time by 40%. ([#1211](https://github.com/httpie/httpie/pull/1211))
|
||||
- Added support for nested JSON syntax. ([#1169](https://github.com/httpie/httpie/issues/1169))
|
||||
- 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 for Terminal 2.6.0 →](https://httpie.io/blog/httpie-2.6.0)
|
||||
|
||||
- Added support for formatting & coloring of JSON bodies preceded by non-JSON data (e.g., an XXSI prefix). ([#1130](https://github.com/httpie/httpie/issues/1130))
|
||||
- Added charset auto-detection when `Content-Type` doesn’t include it. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
- Added `--response-charset` to allow overriding the response encoding for terminal display purposes. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
@ -15,7 +54,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [2.5.0](https://github.com/httpie/httpie/compare/2.4.0...2.5.0) (2021-09-06)
|
||||
|
||||
Blog post: [What’s new in HTTPie 2.5.0](https://httpie.io/blog/httpie-2.5.0)
|
||||
[What’s new in HTTPie for Terminal 2.5.0 →](https://httpie.io/blog/httpie-2.5.0)
|
||||
|
||||
- Added `--raw` to allow specifying the raw request body without extra processing as
|
||||
an alternative to `stdin`. ([#534](https://github.com/httpie/httpie/issues/534))
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
5
Makefile
5
Makefile
@ -130,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/ docs/packaging/brew/ *.py
|
||||
$(VENV_BIN)/flake8 httpie/ tests/ extras/profiling/ docs/packaging/brew/ *.py
|
||||
@echo
|
||||
|
||||
|
||||
@ -206,7 +206,8 @@ brew-test:
|
||||
- brew install --build-from-source ./docs/packaging/brew/httpie.rb
|
||||
|
||||
@echo $(H1)Verifying…$(H1END)
|
||||
brew test httpie
|
||||
http --version
|
||||
https --version
|
||||
|
||||
@echo $(H1)Auditing…$(H1END)
|
||||
brew audit --strict httpie
|
||||
|
679
docs/README.md
679
docs/README.md
@ -58,9 +58,9 @@ Do not edit here, but in docs/installation/.
|
||||
|
||||
### Universal
|
||||
|
||||
#### PyPi
|
||||
#### PyPI
|
||||
|
||||
Please make sure you have Python 3.6 or newer (`python --version`).
|
||||
Please make sure you have Python 3.7 or newer (`python --version`).
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
@ -108,20 +108,6 @@ $ port selfupdate
|
||||
$ port upgrade httpie
|
||||
```
|
||||
|
||||
#### Spack (macOS)
|
||||
|
||||
To install [Spack](https://spack.readthedocs.io/en/latest/index.html), see [its installation](https://spack.readthedocs.io/en/latest/getting_started.html#installation).
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ spack install httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ spack install httpie
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
#### Chocolatey
|
||||
@ -190,13 +176,11 @@ $ apt upgrade httpie
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ dnf update
|
||||
$ dnf install httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ dnf update
|
||||
$ dnf upgrade httpie
|
||||
```
|
||||
|
||||
@ -206,45 +190,15 @@ Also works for other RHEL-derived distributions like ClearOS, Oracle Linux, etc.
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ yum update
|
||||
$ yum install epel-release
|
||||
$ yum install httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ yum update
|
||||
$ yum upgrade httpie
|
||||
```
|
||||
|
||||
#### Alpine Linux
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ apk update
|
||||
$ apk add httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ apk update
|
||||
$ apk add --upgrade httpie
|
||||
```
|
||||
|
||||
#### Gentoo
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ emerge --sync
|
||||
$ emerge httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ emerge --sync
|
||||
$ emerge --update httpie
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
Also works for other Arch-derived distributions like ArcoLinux, EndeavourOS, Artix Linux, etc.
|
||||
@ -259,34 +213,6 @@ $ pacman -Sy httpie
|
||||
$ pacman -Syu httpie
|
||||
```
|
||||
|
||||
#### Void Linux
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ xbps-install -Su
|
||||
$ xbps-install -S httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ xbps-install -Su
|
||||
$ xbps-install -Su httpie
|
||||
```
|
||||
|
||||
#### Spack (Linux)
|
||||
|
||||
To install [Spack](https://spack.readthedocs.io/en/latest/index.html), see [its installation](https://spack.readthedocs.io/en/latest/getting_started.html#installation).
|
||||
|
||||
```bash
|
||||
# Install httpie
|
||||
$ spack install httpie
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade httpie
|
||||
$ spack install httpie
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
#### FreshPorts
|
||||
@ -334,7 +260,7 @@ Verify that now you have the [current development version identifier](https://gi
|
||||
|
||||
```bash
|
||||
$ http --version
|
||||
# 2.6.0
|
||||
# 3.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
@ -445,7 +371,7 @@ There are no restrictions regarding which request methods can include a body. Yo
|
||||
$ http POST pie.dev/post
|
||||
```
|
||||
|
||||
You can also make `GET` requests contaning a body:
|
||||
You can also make `GET` requests containing a body:
|
||||
|
||||
```bash
|
||||
$ http GET pie.dev/get hello=world
|
||||
@ -486,14 +412,26 @@ The default scheme is `http://` and can be omitted from the argument:
|
||||
|
||||
```bash
|
||||
$ http example.org
|
||||
# => http://example.org
|
||||
# → http://example.org
|
||||
```
|
||||
|
||||
HTTPie also installs an `https` executable, where the default scheme is `https://`:
|
||||
|
||||
```bash
|
||||
$ https example.org
|
||||
# => https://example.org
|
||||
# → https://example.org
|
||||
```
|
||||
|
||||
When you paste a URL into the terminal, you can even keep the `://` bit in the URL argument to quickly convert the URL into an HTTPie call just by adding a space after the protocol name.
|
||||
|
||||
```bash
|
||||
$ https ://example.org
|
||||
# → https://example.org
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http ://example.org
|
||||
# → http://example.org
|
||||
```
|
||||
|
||||
### Querystring parameters
|
||||
@ -510,6 +448,12 @@ $ http https://api.github.com/search/repositories q==httpie per_page==1
|
||||
GET /search/repositories?q=httpie&per_page=1 HTTP/1.1
|
||||
```
|
||||
|
||||
You can even retrieve the `value` from a file by using the `param==@file` syntax. This would also effectively strip the newlines from the end. See [#file-based-separators] for more examples.
|
||||
|
||||
```bash
|
||||
$ http pie.dev/get text==@files/text.txt
|
||||
```
|
||||
|
||||
### URL shortcuts for `localhost`
|
||||
|
||||
Additionally, curl-like shorthand for localhost is supported.
|
||||
@ -588,21 +532,48 @@ GET /../../etc/password HTTP/1.1
|
||||
|
||||
## Request items
|
||||
|
||||
There are a few different *request item* types that provide a convenient mechanism for specifying HTTP headers, simple JSON and form data, files, and URL parameters.
|
||||
There are a few different *request item* types that provide a convenient
|
||||
mechanism for specifying HTTP headers, JSON and form data, files,
|
||||
and URL parameters. This is a very practical way of constructing
|
||||
HTTP requests from scratch on the CLI.
|
||||
|
||||
They are key/value pairs specified after the URL. All have in common that they become part of the actual request that is sent and that their type is distinguished only by the separator used: `:`, `=`, `:=`, `==`, `@`, `=@`, and `:=@`. The ones with an `@` expect a file path as value.
|
||||
Each *request item* is simply a key/value pair separated with the following
|
||||
characters: `:` (headers), `=` (data field, e.g., JSON, form), `:=` (raw data field)
|
||||
`==` (query parameters), `@` (file upload).
|
||||
|
||||
```bash
|
||||
$ http PUT pie.dev/put \
|
||||
X-Date:today \ # Header
|
||||
token==secret \ # Query parameter
|
||||
name=John \ # Data field
|
||||
age:=29 # Raw JSON
|
||||
```
|
||||
|
||||
| Item Type | Description |
|
||||
| -----------------------------------------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|-------------------------------------------------------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| HTTP Headers `Name:Value` | Arbitrary HTTP header, e.g. `X-API-Token:123` |
|
||||
| URL parameters `name==value` | Appends the given name/value pair as a querystring parameter to the URL. The `==` separator is used. |
|
||||
| Data Fields `field=value`, `field=@file.txt` | Request data fields to be serialized as a JSON object (default), to be form-encoded (with `--form, -f`), or to be serialized as `multipart/form-data` (with `--multipart`) |
|
||||
| Data Fields `field=value` | Request data fields to be serialized as a JSON object (default), to be form-encoded (with `--form, -f`), or to be serialized as `multipart/form-data` (with `--multipart`) |
|
||||
| Raw JSON fields `field:=json` | Useful when sending JSON and one or more fields need to be a `Boolean`, `Number`, nested `Object`, or an `Array`, e.g., `meals:='["ham","spam"]'` or `pies:=[1,2,3]` (note the quotes) |
|
||||
| File upload fields `field@/dir/file`, `field@file;type=mime` | Only available with `--form`, `-f` and `--multipart`. For example `screenshot@~/Pictures/img.png`, or `'cv@cv.txt;type=text/markdown'`. With `--form`, the presence of a file field results in a `--multipart` request |
|
||||
|
||||
Note that the structured data fields aren’t the only way to specify request data:
|
||||
[raw request body](#raw-request-body) is a mechanism for passing arbitrary request data.
|
||||
|
||||
### File based separators
|
||||
|
||||
Using file contents as values for specific fields is a very common use case, which can be achieved through adding the `@` suffix to
|
||||
the operators above. For example instead of using a static string as the value for some header, you can use `:@` operator
|
||||
to pass the desired value from a file.
|
||||
|
||||
```bash
|
||||
$ http POST pie.dev/post \
|
||||
X-Data:@files/text.txt # Read a header from a file
|
||||
token==@files/text.txt # Read a query parameter from a file
|
||||
name=@files/text.txt # Read a data field’s value from a file
|
||||
bookmarks:=@files/data.json # Embed a JSON object from a file
|
||||
```
|
||||
|
||||
### Escaping rules
|
||||
|
||||
You can use `\` to escape characters that shouldn’t be used as separators (or parts thereof). For instance, `foo\==bar` will become a data key/value pair (`foo=` and `bar`) instead of a URL parameter.
|
||||
@ -627,7 +598,7 @@ Content-Type: application/json
|
||||
JSON is the *lingua franca* of modern web services, and it is also the **implicit content type** HTTPie uses by default.
|
||||
|
||||
Simple example:
|
||||
|
||||
|
||||
```bash
|
||||
$ http PUT pie.dev/put name=John email=john@example.org
|
||||
```
|
||||
@ -653,7 +624,7 @@ Host: pie.dev
|
||||
### Explicit JSON
|
||||
|
||||
You can use `--json, -j` to explicitly set `Accept` to `application/json` regardless of whether you are sending data (it’s a shortcut for setting the header via the usual header notation: `http url Accept:'application/json, */*;q=0.5'`).
|
||||
Additionally, HTTPie will try to detect JSON responses even when the `Content-Type` is incorrectly `text/plain` or unknown.
|
||||
Additionally, HTTPie will try to detect JSON responses even when the `Content-Type` is incorrectly `text/plain` or unknown.
|
||||
|
||||
### Non-string JSON fields
|
||||
|
||||
@ -702,26 +673,311 @@ Host: pie.dev
|
||||
platform[apps][]=Web \
|
||||
platform[apps][]=Mobile
|
||||
```
|
||||
Often the only difference is in adding the `--form, -f` option, which ensures that data fields are serialized as, and `Content-Type` is set to `application/x-www-form-urlencoded; charset=utf-8`.
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": {
|
||||
|
||||
### Regular forms
|
||||
"name": "HTTPie",
|
||||
"about": {
|
||||
"mission": "Make APIs simple and intuitive",
|
||||
"homepage": "httpie.io",
|
||||
"stars": 54000
|
||||
},
|
||||
"apps": [
|
||||
"Terminal",
|
||||
"Desktop",
|
||||
"Web",
|
||||
"Mobile"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Introduction
|
||||
|
||||
Let’s start with a simple example, and build a simple search query:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=id \
|
||||
search[id]:=1
|
||||
```
|
||||
|
||||
In the example above, the `search[type]` is an instruction for creating an object called `search`, and setting the `type` field of it to the given value (`"id"`).
|
||||
|
||||
Also note that, just as the regular syntax, you can use the `:=` operator to directly pass raw JSON values (e.g, numbers in the case above).
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"id": 1,
|
||||
"type": "id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Building arrays is also possible, through `[]` suffix (an append operation). This tells HTTPie to create an array in the given path (if there is not one already), and append the given value to that array.
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=keyword \
|
||||
search[keywords][]=APIs \
|
||||
search[keywords][]=CLI
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"keywords": [
|
||||
"APIs",
|
||||
"CLI"
|
||||
],
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to explicitly specify the position of elements inside an array,
|
||||
you can simply pass the desired index as the path:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=keyword \
|
||||
search[keywords][1]=APIs \
|
||||
search[keywords][0]=CLI
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"keywords": [
|
||||
"CLIs",
|
||||
"API"
|
||||
],
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If there are any missing indexes, HTTPie will nullify them in order to create a concrete object that can be sent:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=platforms \
|
||||
search[platforms][]=Terminal \
|
||||
search[platforms][1]=Desktop \
|
||||
search[platforms][3]=Mobile
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"platforms": [
|
||||
"Terminal",
|
||||
"Desktop",
|
||||
null,
|
||||
"Mobile"
|
||||
],
|
||||
"type": "platforms"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is also possible to embed raw JSON to a nested structure, for example:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
category=tools \
|
||||
search[type]=platforms \
|
||||
'search[platforms]:=["Terminal", "Desktop"]' \
|
||||
search[platforms][]=Web \
|
||||
search[platforms][]=Mobile
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "tools",
|
||||
"search": {
|
||||
"platforms": [
|
||||
"Terminal",
|
||||
"Desktop",
|
||||
"Web",
|
||||
"Mobile"
|
||||
],
|
||||
"type": "platforms"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And just to demonstrate all of these features together, let’s create a very deeply nested JSON object:
|
||||
|
||||
```bash
|
||||
$ http PUT pie.dev/put \
|
||||
shallow=value \ # Shallow key-value pair
|
||||
object[key]=value \ # Nested key-value pair
|
||||
array[]:=1 \ # Array — first item
|
||||
array[1]:=2 \ # Array — second item
|
||||
array[2]:=3 \ # Array — append (third item)
|
||||
very[nested][json][3][httpie][power][]=Amaze # Nested object
|
||||
```
|
||||
|
||||
#### Advanced usage
|
||||
|
||||
##### Escaping behavior
|
||||
|
||||
Nested JSON syntax uses the same [escaping rules](#escaping-rules) as
|
||||
the terminal. There are 3 special characters, and 1 special token that you can escape.
|
||||
|
||||
If you want to send a bracket as is, escape it with a backslash (`\`):
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'foo\[bar\]:=1' \
|
||||
'baz[\[]:=2' \
|
||||
'baz[\]]:=3'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"baz": {
|
||||
"[": 2,
|
||||
"]": 3
|
||||
},
|
||||
"foo[bar]": 1
|
||||
}
|
||||
```
|
||||
|
||||
If you want to send the literal backslash character (`\`), escape it with another backslash:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'backslash[\\]:=1'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"backslash": {
|
||||
"\\": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A regular integer in a path (e.g `[10]`) means an array index; but if you want it to be treated as
|
||||
a string, you can escape the whole number by using a backslash (`\`) prefix.
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'object[\1]=stringified' \
|
||||
'object[\100]=same' \
|
||||
'array[1]=indexified'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"array": [
|
||||
null,
|
||||
"indexified"
|
||||
],
|
||||
"object": {
|
||||
"1": "stringified",
|
||||
"100": "same"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Guiding syntax errors
|
||||
|
||||
If you make a typo or forget to close a bracket, the errors will guide you to fix it. For example:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'foo[bar]=OK' \
|
||||
'foo[baz][quux=FAIL'
|
||||
```
|
||||
|
||||
```console
|
||||
HTTPie Syntax Error: Expecting ']'
|
||||
foo[baz][quux
|
||||
^
|
||||
```
|
||||
|
||||
You can follow to given instruction (adding a `]`) and repair your expression.
|
||||
|
||||
##### Type safety
|
||||
|
||||
Each container path (e.g., `x[y][z]` in `x[y][z][1]`) has a certain type, which gets defined with
|
||||
the first usage and can’t be changed after that. If you try to do a key-based access to an array or
|
||||
an index-based access to an object, HTTPie will error out:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
'array[]:=1' \
|
||||
'array[]:=2' \
|
||||
'array[key]:=3'
|
||||
HTTPie Type Error: Can't perform 'key' based access on 'array' which has a type of 'array' but this operation requires a type of 'object'.
|
||||
array[key]
|
||||
^^^^^
|
||||
```
|
||||
|
||||
Type Safety does not apply to value overrides, for example:
|
||||
|
||||
```bash
|
||||
$ http --offline --print=B pie.dev/post \
|
||||
user[name]:=411 # Defined as an integer
|
||||
user[name]=string # Overridden with a string
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"name": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Raw JSON
|
||||
|
||||
For very complex JSON structures, it may be more convenient to [pass it as raw request body](#raw-request-body), for example:
|
||||
|
||||
```bash
|
||||
$ echo -n '{"hello": "world"}' | http POST pie.dev/post
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http POST pie.dev/post < files/data.json
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
Submitting forms is very similar to sending [JSON](#json) requests.
|
||||
Often the only difference is in adding the `--form, -f` option, which ensures that data fields are serialized as, and `Content-Type` is set to `application/x-www-form-urlencoded; charset=utf-8`.
|
||||
It is possible to make form data the implicit content type instead of JSON via the [config](#config) file.
|
||||
|
||||
### Regular forms
|
||||
|
||||
```bash
|
||||
$ http --form POST pie.dev/post name='John Smith'
|
||||
```
|
||||
|
||||
```http
|
||||
POST /post HTTP/1.1
|
||||
Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
|
||||
name=John+Smith
|
||||
```
|
||||
|
||||
```http
|
||||
POST /post HTTP/1.1
|
||||
Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
|
||||
### File upload forms
|
||||
|
||||
If one or more file fields is present, the serialization and content type is `multipart/form-data`:
|
||||
|
||||
|
||||
If one or more file fields is present, the serialization and content type is `multipart/form-data`:
|
||||
|
||||
```bash
|
||||
$ http -f POST pie.dev/post name='John Smith' cv@~/files/data.xml
|
||||
```
|
||||
@ -787,7 +1043,7 @@ world
|
||||
|
||||
```bash
|
||||
$ http --form --multipart --offline example.org hello=world Content-Type:multipart/letter
|
||||
```
|
||||
```
|
||||
|
||||
```http
|
||||
POST / HTTP/1.1
|
||||
@ -859,6 +1115,14 @@ Host: <taken-from-URL>
|
||||
```
|
||||
|
||||
Please note that some internal headers, such as `Content-Length`, can’t be unset if
|
||||
they are automatically added by the client itself.
|
||||
|
||||
### Multiple header values with the same name
|
||||
|
||||
If the request is sent with multiple headers that are sharing the same name, then
|
||||
the HTTPie will send them individually.
|
||||
|
||||
```bash
|
||||
http --offline example.org Cookie:one Cookie:two
|
||||
```
|
||||
|
||||
@ -873,6 +1137,39 @@ To send a header with an empty value, use `Header;`, with a semicolon:
|
||||
|
||||
```bash
|
||||
http --offline example.org Numbers:one,two
|
||||
```
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
Numbers: one,two
|
||||
```
|
||||
|
||||
Also be aware that if the current session contains any headers they will get overwritten
|
||||
by individual commands when sending a request instead of being joined together.
|
||||
|
||||
### Limiting response headers
|
||||
|
||||
The `--max-headers=n` options allows you to control the number of headers HTTPie reads before giving up (the default `0`, i.e., there’s no limit).
|
||||
|
||||
```bash
|
||||
$ http --max-headers=100 pie.dev/get
|
||||
```
|
||||
|
||||
## Offline mode
|
||||
|
||||
Use `--offline` to construct HTTP requests without sending them anywhere.
|
||||
With `--offline`, HTTPie builds a request based on the specified options and arguments, prints it to `stdout`, and then exits. It works completely offline; no network connection is ever made. This has a number of use cases, including:
|
||||
|
||||
Generating API documentation examples that you can copy & paste without sending a request:
|
||||
|
||||
```bash
|
||||
$ http --offline POST server.chess/api/games API-Key:ZZZ w=magnus b=hikaru t=180 i=2
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http --offline MOVE server.chess/api/games/123 API-Key:ZZZ p=b a=R1a3 t=77
|
||||
```
|
||||
|
||||
Generating raw requests that can be sent with any other client:
|
||||
|
||||
```bash
|
||||
@ -910,7 +1207,7 @@ $ nc pie.dev 80 < request.http
|
||||
Cookie: sessionid=foo
|
||||
Host: pie.dev
|
||||
User-Agent: HTTPie/0.9.9
|
||||
```
|
||||
```
|
||||
|
||||
Send multiple cookies (note: the header is quoted to prevent the shell from interpreting the `;`):
|
||||
|
||||
@ -959,9 +1256,9 @@ the [sessions](#sessions) feature.
|
||||
```
|
||||
|
||||
### Password prompt
|
||||
|
||||
```bash
|
||||
$ http -a username: pie.dev/headers
|
||||
|
||||
If you omit the password part of `--auth, -a`, HTTPie securely prompts you for it:
|
||||
|
||||
```bash
|
||||
$ http -a username pie.dev/basic-auth/username/password
|
||||
```
|
||||
@ -975,14 +1272,26 @@ $ http -a username:password pie.dev/basic-auth/username/password
|
||||
```bash
|
||||
$ http -a username: pie.dev/headers
|
||||
```
|
||||
|
||||
### `.netrc`
|
||||
|
||||
Authentication information from your `~/.netrc` file is by default honored as well.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ cat ~/.netrc
|
||||
machine pie.dev
|
||||
login httpie
|
||||
password test
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http pie.dev/basic-auth/httpie/test
|
||||
HTTP/1.1 200 OK
|
||||
[...]
|
||||
```
|
||||
|
||||
This can be disabled with the `--ignore-netrc` option:
|
||||
|
||||
```bash
|
||||
@ -1030,6 +1339,8 @@ Here are a few picks:
|
||||
With `307 Temporary Redirect` and `308 Permanent Redirect`, the method and the body of the original request
|
||||
are reused to perform the redirected request. Otherwise, a body-less `GET` request is performed.
|
||||
|
||||
### Showing intermediary redirect responses
|
||||
|
||||
If you wish to see the intermediary requests/responses,
|
||||
then use the `--all` option:
|
||||
|
||||
@ -1169,12 +1480,14 @@ message is printed (headers as well as the body). You can control what should
|
||||
| `m` | [response meta](#response-meta) |
|
||||
|
||||
Print request and response headers:
|
||||
```
|
||||
|
||||
```bash
|
||||
$ http --print=Hh PUT pie.dev/put hello=world
|
||||
```
|
||||
|
||||
#### Response meta
|
||||
|
||||
|
||||
The response metadata section currently includes the total time elapsed. It’s the number of seconds between opening the network connection and downloading the last byte of response the body.
|
||||
|
||||
|
||||
To _only_ show the response metadata, use `--meta, -m` (analogically to `--headers, -h` and `--body, -b`):
|
||||
@ -1182,11 +1495,12 @@ All the other [output options](#output-options) are under the hood just shortcut
|
||||
```bash
|
||||
$ http --meta pie.dev/delay/1
|
||||
```
|
||||
User-Agent: HTTPie/0.2.7dev
|
||||
|
||||
```console
|
||||
Elapsed time: 1.099171542s
|
||||
```
|
||||
|
||||
The [extra verbose `-vv` output](#extra-verbose-output) includes the meta section by default. You can also show it in combination with other parts of the exchange via [`--print=m`](#what-parts-of-the-http-exchange-should-be-printed). For example, here we print it together with the response headers:
|
||||
|
||||
```bash
|
||||
$ http --print=hm pie.dev/get
|
||||
@ -1194,6 +1508,40 @@ Print request and response headers:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
|
||||
Please note that it also includes time spent on formatting the output, which adds a small penalty. Also, if the body is not part of the output, [we don’t spend time downloading it](#conditional-body-download).
|
||||
|
||||
If you [use `--style` with one of the Pie themes](#colors-and-formatting), you’ll see the time information color-coded (green/yellow/orange/red) based on how long the exchange took.
|
||||
|
||||
|
||||
### Verbose output
|
||||
|
||||
`--verbose` can often be useful for debugging the request and generating documentation examples:
|
||||
|
||||
```bash
|
||||
$ http --verbose PUT pie.dev/put hello=world
|
||||
PUT /put HTTP/1.1
|
||||
Accept: application/json, */*;q=0.5
|
||||
Accept-Encoding: gzip, deflate
|
||||
Content-Type: application/json
|
||||
Host: pie.dev
|
||||
User-Agent: HTTPie/0.2.7dev
|
||||
|
||||
{
|
||||
"hello": "world"
|
||||
}
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Connection: keep-alive
|
||||
Content-Length: 477
|
||||
Content-Type: application/json
|
||||
Date: Sun, 05 Aug 2012 00:25:23 GMT
|
||||
Server: gunicorn/0.13.4
|
||||
|
||||
{
|
||||
[…]
|
||||
}
|
||||
@ -1223,6 +1571,15 @@ Server: gunicorn/0.13.4
|
||||
```bash
|
||||
# There will be no output, even in case of an unexpected response status code:
|
||||
$ http -qq --check-status pie.dev/post enjoy='the silence without warnings'
|
||||
```
|
||||
|
||||
### Viewing intermediary requests/responses
|
||||
|
||||
To see all the HTTP communication, i.e. the final request/response as well as any possible intermediary requests/responses, use the `--all` option.
|
||||
The intermediary HTTP communication include followed redirects (with `--follow`), the first unauthorized request when HTTP digest authentication is used (`--auth=digest`), etc.
|
||||
|
||||
```bash
|
||||
# Include all responses that lead to the final one:
|
||||
$ http --all --follow pie.dev/redirect/3
|
||||
```
|
||||
|
||||
@ -1280,7 +1637,7 @@ The response headers are downloaded always, even if they are not part of the out
|
||||
Or the output of another program:
|
||||
|
||||
```bash
|
||||
```bash
|
||||
$ grep '401 Unauthorized' /var/log/httpd/error_log | http POST pie.dev/post
|
||||
```
|
||||
|
||||
You can use `echo` for simple data:
|
||||
@ -1346,7 +1703,7 @@ On macOS, you can send the contents of the clipboard with `pbpaste`:
|
||||
|
||||
### Request data from a filename
|
||||
|
||||
|
||||
An alternative to redirected `stdin` is specifying a filename (as `@/path/to/file`) whose content is used as if it came from `stdin`.
|
||||
|
||||
It has the advantage that the `Content-Type` header is automatically set to the appropriate value based on the filename extension.
|
||||
For example, the following request sends the verbatim contents of that XML file with `Content-Type: application/xml`:
|
||||
@ -1426,13 +1783,16 @@ HTTPie does several things by default in order to make its terminal output easy
|
||||
| `--pretty=none` | Disables output processing. Default for redirected output |
|
||||
|
||||
HTTPie looks at `Content-Type` to select the right syntax highlighter and formatter for each message body. If that fails (e.g., the server provides the wrong type), or you prefer a different treatment, you can manually overwrite the mime type for a response with `--response-mime`:
|
||||
$ http --response-mime=text/yaml pie.dev/get
|
||||
|
||||
```bash
|
||||
$ http --response-mime=text/yaml pie.dev/get
|
||||
```
|
||||
|
||||
|
||||
Formatting has the following effects:
|
||||
|
||||
- HTTP headers are sorted by name.
|
||||
- JSON data is indented, sorted by keys, and unicode escapes are converted
|
||||
to the characters they represent.
|
||||
- XML and XHTML data is indented.
|
||||
|
||||
Please note that sometimes there might be changes made by formatters on the actual response body (e.g.,
|
||||
@ -1440,7 +1800,7 @@ There are dozens of styles available, here are just a few notable ones:
|
||||
these formatting changes can be configured more granularly through [format options](#format-options).
|
||||
|
||||
### Format options
|
||||
The `--format-options=opt1:value,opt2:value` option allows you to control how the output should be formatted
|
||||
|
||||
The `--format-options=opt1:value,opt2:value` option allows you to control how the output should be formatted
|
||||
when formatting is applied. The following options are available:
|
||||
|
||||
@ -1459,7 +1819,9 @@ Formatting has the following effects:
|
||||
```bash
|
||||
$ http --format-options headers.sort:false,json.sort_keys:false,json.indent:2 pie.dev/get
|
||||
```
|
||||
There are also two shortcuts that allow you to quickly disable and re-enable
|
||||
|
||||
There are also two shortcuts that allow you to quickly disable and re-enable
|
||||
sorting-related format options (currently it means JSON keys and headers):
|
||||
`--unsorted` and `--sorted`.
|
||||
|
||||
This is something you will typically store as one of the default options in your [config](#config) file.
|
||||
@ -1467,7 +1829,7 @@ The `--format-options=opt1:value,opt2:value` option allows you to control how th
|
||||
### Redirected output
|
||||
|
||||
HTTPie uses a different set of defaults for redirected output than for [terminal output](#terminal-output).
|
||||
HTTPie uses a different set of defaults for redirected output than for [terminal output](#terminal-output).
|
||||
The differences being:
|
||||
|
||||
- Formatting and colors aren’t applied (unless `--pretty` is specified).
|
||||
- Only the response body is printed (unless one of the [output options](#output-options) is set).
|
||||
@ -1616,7 +1978,7 @@ $ http -dco file.zip example.org/file
|
||||
|
||||
Streamed output by small chunks à la `tail -f`:
|
||||
|
||||
# Send each new line (JSON object) to another URL as soon as it arrives from a streaming API:
|
||||
```bash
|
||||
# Send each new line (JSON object) to another URL as soon as it arrives from a streaming API:
|
||||
$ http --stream pie.dev/stream/3 | while read line; do echo "$line" | http pie.dev/post ; done
|
||||
```
|
||||
@ -1631,6 +1993,8 @@ You can use the `--stream, -S` flag to make two things happen:
|
||||
```bash
|
||||
# Create a new session:
|
||||
$ http --session=./session.json pie.dev/headers API-Token:123
|
||||
```
|
||||
|
||||
```bash
|
||||
# Inspect / edit the generated session file:
|
||||
$ cat session.json
|
||||
@ -1668,7 +2032,7 @@ $ cat session.json
|
||||
|
||||
Named sessions’ data is stored in JSON files inside the `sessions` subdirectory of the [config](#config) directory, typically `~/.config/httpie/sessions/<host>/<name>.json` (`%APPDATA%\httpie\sessions\<host>\<name>.json` on Windows).
|
||||
|
||||
|
||||
If you have executed the above commands on a Unix machine, you should be able to list the generated sessions files using:
|
||||
|
||||
```bash
|
||||
$ ls -l ~/.config/httpie/sessions/pie.dev
|
||||
@ -1695,7 +2059,7 @@ $ http --session=user2 -a user2:password pie.dev/get X-Bar:Foo
|
||||
```
|
||||
|
||||
When creating anonymous sessions, please remember to always include at least one `/`, even if the session files is located in the current directory (i.e. `--session=./session.json` instead of just `--session=session.json`), otherwise HTTPie assumes a named session instead.
|
||||
When creating anonymous sessions, please remember to always include at least one `/`, even if the session files is located in the current directory (i.e. `--session=./session.json` instead of just `--session=session.json`), otherwise HTTPie assumes a named session instead.
|
||||
|
||||
### Readonly session
|
||||
|
||||
To use the original session file without updating it from the request/response exchange after it has been created, specify the session name via `--session-read-only=SESSION_NAME_OR_PATH` instead.
|
||||
@ -1791,7 +2155,7 @@ If the server expires an existing cookie, it will also be removed from the sessi
|
||||
|
||||
An `Array` (by default empty) of default options that should be applied to every invocation of HTTPie.
|
||||
|
||||
|
||||
For instance, you can use this config option to change your default color theme:
|
||||
|
||||
```bash
|
||||
$ cat ~/.config/httpie/config.json
|
||||
@ -1833,7 +2197,12 @@ $ cat ~/.config/httpie/config.json
|
||||
2) echo 'Request timed out!' ;;
|
||||
3) echo 'Unexpected HTTP 3xx Redirection!' ;;
|
||||
4) echo 'HTTP 4xx Client Error!' ;;
|
||||
fi
|
||||
5) echo 'HTTP 5xx Server Error!' ;;
|
||||
6) echo 'Exceeded --max-redirects=<n> redirects!' ;;
|
||||
*) echo 'Other Error!' ;;
|
||||
esac
|
||||
fi
|
||||
```
|
||||
|
||||
### Best practices
|
||||
|
||||
@ -1874,6 +2243,79 @@ And since there’s neither data nor `EOF`, it will get stuck. So unless you’r
|
||||
By default, the plugins (and their missing dependencies) will be stored under the configuration directory,
|
||||
but this can be modified through `plugins_dir` variable on the config.
|
||||
|
||||
#### `httpie plugins install`
|
||||
|
||||
For installing plugins from [PyPI](https://pypi.org/) or from local paths, `httpie plugins install`
|
||||
can be used.
|
||||
|
||||
```bash
|
||||
$ httpie plugins install httpie-plugin
|
||||
Installing httpie-plugin...
|
||||
Successfully installed httpie-plugin-1.0.2
|
||||
```
|
||||
|
||||
> Tip: Generally HTTPie plugins start with `httpie-` prefix. Try searching for it on [PyPI](https://pypi.org/search/?q=httpie-)
|
||||
> to find out all plugins from the community.
|
||||
|
||||
#### `httpie plugins list`
|
||||
|
||||
List all installed plugins.
|
||||
|
||||
```bash
|
||||
$ httpie plugins list
|
||||
httpie_plugin (1.0.2)
|
||||
httpie_plugin (httpie.plugins.auth.v1)
|
||||
httpie_plugin_2 (1.0.6)
|
||||
httpie_plugin_2 (httpie.plugins.auth.v1)
|
||||
httpie_converter (1.0.0)
|
||||
httpie_iterm_converter (httpie.plugins.converter.v1)
|
||||
httpie_konsole_konverter (httpie.plugins.converter.v1)
|
||||
```
|
||||
|
||||
#### `httpie plugins upgrade`
|
||||
|
||||
For upgrading already installed plugins, use `httpie plugins upgrade`.
|
||||
|
||||
```bash
|
||||
$ httpie plugins upgrade httpie-plugin
|
||||
```
|
||||
|
||||
#### `httpie plugins uninstall`
|
||||
|
||||
Uninstall plugins from the isolated plugins directory. If the plugin is not installed
|
||||
through `httpie plugins install`, it won’t uninstall it.
|
||||
|
||||
```bash
|
||||
$ httpie plugins uninstall httpie-plugin
|
||||
```
|
||||
|
||||
## Meta
|
||||
|
||||
### Interface design
|
||||
|
||||
The syntax of the command arguments closely correspond to the actual HTTP requests sent over the wire.
|
||||
It has the advantage that it’s easy to remember and read.
|
||||
You can often translate an HTTP request to an HTTPie argument list just by inlining the request elements.
|
||||
For example, compare this HTTP request:
|
||||
|
||||
```http
|
||||
POST /post HTTP/1.1
|
||||
Host: pie.dev
|
||||
X-API-Key: 123
|
||||
User-Agent: Bacon/1.0
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
name=value&name2=value2
|
||||
```
|
||||
|
||||
with the HTTPie command that sends it:
|
||||
|
||||
```bash
|
||||
$ http -f POST pie.dev/post \
|
||||
X-API-Key:123 \
|
||||
User-Agent:Bacon/1.0 \
|
||||
name=value \
|
||||
name2=value2
|
||||
```
|
||||
|
||||
Notice that both the order of elements and the syntax are very similar, and that only a small portion of the command is used to control HTTPie and doesn’t directly correspond to any part of the request (here, it’s only `-f` asking HTTPie to send a form request).
|
||||
@ -1917,7 +2359,6 @@ HTTPie has the following community channels:
|
||||
- [curl](https://curl.haxx.se) — a "Swiss knife" command line tool and an exceptional library for transferring data with URLs.
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
See [CONTRIBUTING](https://github.com/httpie/httpie/blob/master/CONTRIBUTING.md).
|
||||
|
||||
@ -1954,7 +2395,7 @@ See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md).
|
||||
|
||||
### Artwork
|
||||
|
||||
- [README Animation](https://raw.githubusercontent.com/httpie/httpie/master/httpie.gif) by [Allen Smith](https://github.com/loranallensmith).
|
||||
- [README Animation](https://github.com/httpie/httpie/blob/master/docs/httpie-animation.gif) by [Allen Smith](https://github.com/loranallensmith).
|
||||
|
||||
### Licence
|
||||
|
||||
@ -1962,4 +2403,4 @@ BSD-3-Clause: [LICENSE](https://github.com/httpie/httpie/blob/master/LICENSE).
|
||||
|
||||
### Authors
|
||||
|
||||
[Jakub Roztocil](https://roztocil.co) ([@jakubroztocil](https://twitter.com/jakubroztocil)) created HTTPie and [these fine people](https://github.com/httpie/httpie/AUTHORS.md) have contributed.
|
||||
[Jakub Roztocil](https://roztocil.co) ([@jakubroztocil](https://twitter.com/jakubroztocil)) created HTTPie and [these fine people](https://github.com/httpie/httpie/blob/master/AUTHORS.md) have contributed.
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"website": {
|
||||
"master_and_released_docs_differ_after": "8f8851f1dbd511d3bc0ea0f6da7459045610afce"
|
||||
"master_and_released_docs_differ_after": null
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,11 @@ from jinja2 import Template
|
||||
from fetch import HERE, load_awesome_people
|
||||
|
||||
TPL_FILE = HERE / 'snippet.jinja2'
|
||||
HTTPIE_TEAM = {'jakubroztocil', 'BoboTiG', 'claudiatd'}
|
||||
HTTPIE_TEAM = {
|
||||
'claudiatd',
|
||||
'jakubroztocil',
|
||||
'jkbr',
|
||||
}
|
||||
|
||||
|
||||
def generate_snippets(release: str) -> str:
|
||||
|
@ -1,12 +1,48 @@
|
||||
{
|
||||
"Aaron Miller": {
|
||||
"committed": [],
|
||||
"github": "aaronhmiller",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "aaronmiller8"
|
||||
},
|
||||
"Alexander Bogdanov": {
|
||||
"committed": [],
|
||||
"github": "ab-kily",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Almad": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "Almad",
|
||||
"reported": [],
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "almadcz"
|
||||
},
|
||||
"Andr\u00e1s Czig\u00e1ny": {
|
||||
"committed": [],
|
||||
"github": "andrascz",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Annette Wilson": {
|
||||
"committed": [],
|
||||
"github": "annettejanewilson",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Anton Emelyanov": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -15,6 +51,32 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Batuhan Taskaya": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "isidentical",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "isidentical"
|
||||
},
|
||||
"Brad Crittenden": {
|
||||
"committed": [],
|
||||
"github": "bac",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Chad": {
|
||||
"committed": [],
|
||||
"github": "cythrawll",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"D8ger": {
|
||||
"committed": [],
|
||||
"github": "caofanCPU",
|
||||
@ -23,6 +85,15 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Dave": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "davecheney",
|
||||
"reported": [],
|
||||
"twitter": "davecheney"
|
||||
},
|
||||
"Dawid Ferenczy Rogo\u017ean": {
|
||||
"committed": [],
|
||||
"github": "ferenczy",
|
||||
@ -31,6 +102,14 @@
|
||||
],
|
||||
"twitter": "DawidFerenczy"
|
||||
},
|
||||
"Ed Rooth": {
|
||||
"committed": [],
|
||||
"github": "sym3tri",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Elena Lape": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -39,6 +118,15 @@
|
||||
"reported": [],
|
||||
"twitter": "elena_lape"
|
||||
},
|
||||
"Fabio Peruzzo": {
|
||||
"committed": [],
|
||||
"github": "peruzzof",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"F\u00fash\u0113ng": {
|
||||
"committed": [],
|
||||
"github": "lienide",
|
||||
@ -47,6 +135,22 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Gabriel Cruz": {
|
||||
"committed": [],
|
||||
"github": "gmelodie",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "gmelodiecruz"
|
||||
},
|
||||
"Gaurav": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "gkcs",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Giampaolo Rodola": {
|
||||
"committed": [],
|
||||
"github": "giampaolo",
|
||||
@ -55,6 +159,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Greg Myers": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "myersg86",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Hugh Williams": {
|
||||
"committed": [],
|
||||
"github": "hughpv",
|
||||
@ -75,20 +187,34 @@
|
||||
},
|
||||
"Jakub Roztocil": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "jakubroztocil",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "jakubroztocil"
|
||||
},
|
||||
"Jan Bra\u0161na": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "janbrasna",
|
||||
"reported": [],
|
||||
"twitter": "janbrasna"
|
||||
},
|
||||
"Jan Verbeek": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
],
|
||||
"github": "blyxxyz",
|
||||
"reported": [],
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Jannik Vieten": {
|
||||
@ -99,6 +225,22 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Jesper Holmberg": {
|
||||
"committed": [],
|
||||
"github": "strindberg",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Kirill Krasnov": {
|
||||
"committed": [],
|
||||
"github": "Kirill",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Marcel St\u00f6r": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -107,6 +249,14 @@
|
||||
"reported": [],
|
||||
"twitter": "frightanic"
|
||||
},
|
||||
"Marco Seguri": {
|
||||
"committed": [],
|
||||
"github": "seguri",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Mariano Ruiz": {
|
||||
"committed": [],
|
||||
"github": "mrsarm",
|
||||
@ -115,19 +265,41 @@
|
||||
],
|
||||
"twitter": "mrsarm82"
|
||||
},
|
||||
"Mark Rosenbaum": {
|
||||
"committed": [],
|
||||
"github": "markrosenbaum",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Micka\u00ebl Schoentgen": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "BoboTiG",
|
||||
"reported": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "__tiger222__"
|
||||
},
|
||||
"Mike DePalatis": {
|
||||
"committed": [],
|
||||
"github": "mivade",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Miro Hron\u010dok": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
"2.5.0",
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "hroncok",
|
||||
"reported": [],
|
||||
@ -141,6 +313,42 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Nanashi.": {
|
||||
"committed": [],
|
||||
"github": "sevenc-nanashi",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "sevenc_nanashi"
|
||||
},
|
||||
"Omer Akram": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "om26er",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "om26er"
|
||||
},
|
||||
"Patrick Taylor": {
|
||||
"committed": [],
|
||||
"github": "pmeister",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Paul Laffitte": {
|
||||
"committed": [],
|
||||
"github": "paullaffitte",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": "plaffitt"
|
||||
},
|
||||
"Pavel Alexeev aka Pahan-Hubbitus": {
|
||||
"committed": [],
|
||||
"github": "Hubbitus",
|
||||
@ -149,6 +357,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Russell Shurts": {
|
||||
"committed": [],
|
||||
"github": "rshurts",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Samuel Marks": {
|
||||
"committed": [],
|
||||
"github": "SamuelMarks",
|
||||
@ -157,6 +373,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Sebastian Czech": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "sebastianczech",
|
||||
"reported": [],
|
||||
"twitter": "sebaczech"
|
||||
},
|
||||
"Sullivan SENECHAL": {
|
||||
"committed": [],
|
||||
"github": "soullivaneuh",
|
||||
@ -173,6 +397,39 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Vincent van \u2019t Zand": {
|
||||
"committed": [],
|
||||
"github": "vovtz",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Vivaan Verma": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "doublevcodes",
|
||||
"reported": [],
|
||||
"twitter": "doublevcodes"
|
||||
},
|
||||
"Vladimir Berkutov": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "dair-targ",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"Will Rogers": {
|
||||
"committed": [],
|
||||
"github": "wjrogers",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"Yannic Schneider": {
|
||||
"committed": [],
|
||||
"github": "cynay",
|
||||
@ -189,6 +446,14 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"arloan": {
|
||||
"committed": [],
|
||||
"github": "arloan",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"bl-ue": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -197,6 +462,48 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"blueray453": {
|
||||
"committed": [],
|
||||
"github": "blueray453",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"claudiatd": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "claudiatd",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"coldcoff": {
|
||||
"committed": [],
|
||||
"github": "coldcoff",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"dkreeft": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "dkreeft",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"greg": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "gregkh",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"henryhu712": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -205,6 +512,31 @@
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"hosseingt": {
|
||||
"committed": [
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "hosseingt",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"jakubroztocil": {
|
||||
"committed": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"github": "jkbr",
|
||||
"reported": [],
|
||||
"twitter": null
|
||||
},
|
||||
"josephworks": {
|
||||
"committed": [],
|
||||
"github": "josephworks",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"jungle-boogie": {
|
||||
"committed": [],
|
||||
"github": "jungle-boogie",
|
||||
@ -213,6 +545,14 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"luisuimi": {
|
||||
"committed": [],
|
||||
"github": "luisuimi",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"nixbytes": {
|
||||
"committed": [
|
||||
"2.5.0"
|
||||
@ -221,6 +561,14 @@
|
||||
"reported": [],
|
||||
"twitter": "linuxbyte3"
|
||||
},
|
||||
"peterpt": {
|
||||
"committed": [],
|
||||
"github": "peterpt",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"qiulang": {
|
||||
"committed": [],
|
||||
"github": "qiulang",
|
||||
@ -229,6 +577,30 @@
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"stonebig": {
|
||||
"committed": [],
|
||||
"github": "stonebig",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"whodidthis": {
|
||||
"committed": [],
|
||||
"github": "whodidthis",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zoulja": {
|
||||
"committed": [],
|
||||
"github": "zoulja",
|
||||
"reported": [
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"zwx00": {
|
||||
"committed": [],
|
||||
"github": "zwx00",
|
||||
@ -236,5 +608,23 @@
|
||||
"2.5.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"\u5d14\u5c0f\u4e8c": {
|
||||
"committed": [],
|
||||
"github": "rogerdehe",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
},
|
||||
"\u9ec4\u6d77": {
|
||||
"committed": [],
|
||||
"github": "hh-in-zhuzhou",
|
||||
"reported": [
|
||||
"2.6.0",
|
||||
"3.0.0"
|
||||
],
|
||||
"twitter": null
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ docs-structure:
|
||||
macOS:
|
||||
- brew-mac
|
||||
- port
|
||||
- spack-mac
|
||||
Windows:
|
||||
- chocolatey
|
||||
Linux:
|
||||
@ -23,29 +22,11 @@ docs-structure:
|
||||
- apt
|
||||
- dnf
|
||||
- yum
|
||||
- apk
|
||||
- emerge
|
||||
- pacman
|
||||
- xbps-install
|
||||
- spack-linux
|
||||
FreeBSD:
|
||||
- pkg
|
||||
|
||||
tools:
|
||||
apk:
|
||||
title: Alpine Linux
|
||||
name: apk
|
||||
links:
|
||||
homepage: https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management
|
||||
package: https://pkgs.alpinelinux.org/package/edge/community/x86/httpie
|
||||
commands:
|
||||
install:
|
||||
- apk update
|
||||
- apk add httpie
|
||||
upgrade:
|
||||
- apk update
|
||||
- apk add --upgrade httpie
|
||||
|
||||
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.
|
||||
@ -112,26 +93,10 @@ tools:
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- dnf update
|
||||
- dnf install httpie
|
||||
upgrade:
|
||||
- dnf update
|
||||
- dnf upgrade httpie
|
||||
|
||||
emerge:
|
||||
title: Gentoo
|
||||
name: Portage
|
||||
links:
|
||||
homepage: https://wiki.gentoo.org/wiki/Portage
|
||||
package: https://packages.gentoo.org/packages/net-misc/httpie
|
||||
commands:
|
||||
install:
|
||||
- emerge --sync
|
||||
- emerge httpie
|
||||
upgrade:
|
||||
- emerge --sync
|
||||
- emerge --update httpie
|
||||
|
||||
pacman:
|
||||
title: Arch Linux
|
||||
name: pacman
|
||||
@ -173,9 +138,9 @@ tools:
|
||||
- port upgrade httpie
|
||||
|
||||
pypi:
|
||||
title: PyPi
|
||||
title: PyPI
|
||||
name: pip
|
||||
note: Please make sure you have Python 3.6 or newer (`python --version`).
|
||||
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/
|
||||
@ -201,43 +166,6 @@ tools:
|
||||
upgrade:
|
||||
- snap refresh httpie
|
||||
|
||||
spack-linux:
|
||||
title: Spack (Linux)
|
||||
name: Spack
|
||||
links:
|
||||
homepage: https://spack.readthedocs.io/en/latest/index.html
|
||||
setup: https://spack.readthedocs.io/en/latest/getting_started.html#installation
|
||||
commands:
|
||||
install:
|
||||
- spack install httpie
|
||||
upgrade:
|
||||
- spack install httpie
|
||||
|
||||
spack-mac:
|
||||
title: Spack (macOS)
|
||||
name: Spack
|
||||
links:
|
||||
homepage: https://spack.readthedocs.io/en/latest/index.html
|
||||
setup: https://spack.readthedocs.io/en/latest/getting_started.html#installation
|
||||
commands:
|
||||
install:
|
||||
- spack install httpie
|
||||
upgrade:
|
||||
- spack install httpie
|
||||
|
||||
xbps-install:
|
||||
title: Void Linux
|
||||
name: XBPS
|
||||
links:
|
||||
homepage: https://docs.voidlinux.org/xbps/index.html
|
||||
commands:
|
||||
install:
|
||||
- xbps-install -Su
|
||||
- xbps-install -S httpie
|
||||
upgrade:
|
||||
- xbps-install -Su
|
||||
- xbps-install -Su httpie
|
||||
|
||||
yum:
|
||||
title: CentOS and RHEL
|
||||
name: Yum
|
||||
@ -247,9 +175,7 @@ tools:
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- yum update
|
||||
- yum install epel-release
|
||||
- yum install httpie
|
||||
upgrade:
|
||||
- yum update
|
||||
- yum upgrade httpie
|
||||
|
@ -17,6 +17,12 @@ exclude_rule 'MD013'
|
||||
# MD014 Dollar signs used before commands without showing output
|
||||
exclude_rule 'MD014'
|
||||
|
||||
# MD028 Blank line inside blockquote
|
||||
exclude_rule 'MD028'
|
||||
|
||||
# MD012 Multiple consecutive blank lines
|
||||
exclude_rule 'MD012'
|
||||
|
||||
# Tell the linter to use ordered lists:
|
||||
# 1. Foo
|
||||
# 2. Bar
|
||||
|
@ -12,11 +12,11 @@ You are looking at the HTTPie packaging documentation, where you will find valua
|
||||
|
||||
The overall release process starts simple:
|
||||
|
||||
1. Do the [PyPi](https://pypi.org/project/httpie/) publication.
|
||||
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
|
||||
## 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).
|
||||
@ -24,6 +24,7 @@ That is done quite easily by manually triggering the [release workflow](https://
|
||||
## 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
|
||||
@ -33,18 +34,13 @@ A more complete state of deployment can be found on [repology](https://repology.
|
||||
|
||||
| OS | Maintainer |
|
||||
| -------------------------------------------: | -------------- |
|
||||
| [Alpine](linux-alpine/) | **HTTPie** |
|
||||
| [Arch Linux, and derived](linux-arch/) | trusted person |
|
||||
| :construction: [AOSC OS](linux-aosc/) | **HTTPie** |
|
||||
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
|
||||
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
|
||||
| [Fedora](linux-fedora/) | trusted person |
|
||||
| [Gentoo](linux-gentoo/) | **HTTPie** |
|
||||
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
||||
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
|
||||
| [Snapcraft](snapcraft/) | **HTTPie** |
|
||||
| [Spack](spack/) | **HTTPie** |
|
||||
| [Void Linux](linux-void/) | **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/).
|
||||
|
@ -26,7 +26,7 @@ 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]([ file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb)).
|
||||
Then, open a pull request with those changes to the [downstream file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb).
|
||||
|
||||
## Hacking
|
||||
|
||||
|
@ -31,6 +31,7 @@ PACKAGES = [
|
||||
'requests',
|
||||
'requests-toolbelt',
|
||||
'urllib3',
|
||||
'multidict',
|
||||
]
|
||||
|
||||
|
||||
|
@ -3,29 +3,30 @@ class Httpie < Formula
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.io/"
|
||||
url "https://files.pythonhosted.org/packages/90/64/7ea8066309970f787653bdc8c5328272a5c4d06cbce3a07a6a5c3199c3d7/httpie-2.5.0.tar.gz"
|
||||
sha256 "fe6a8bc50fb0635a84ebe1296a732e39357c3e1354541bf51a7057b4877e47f9"
|
||||
url "https://files.pythonhosted.org/packages/7b/f9/13070f19226b7db3641fb787df36bb715063abe1b8ca03fbaeca0f465d27/httpie-3.0.1.tar.gz"
|
||||
sha256 "0e9bc93ebdcdd2d32ec24b8fa46cf7e4fde9eec7a6bd0c5d0ef224f25d7466b2"
|
||||
license "BSD-3-Clause"
|
||||
head "https://github.com/httpie/httpie.git"
|
||||
head "https://github.com/httpie/httpie.git", branch: "master"
|
||||
|
||||
bottle do
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "01115f69aff0399b3f73af09899a42a14343638a4624a35749059cc732c49cdc"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "53f07157f00edf8193b7d4f74f247f53e1796fbc3e675cd2fbaa4b9dc2bad62c"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "7cf216fdee98208856d654060fdcad3968623d7ed27fcdeba27d3120354c9a9f"
|
||||
sha256 cellar: :any_skip_relocation, mojave: "28adb5aed8c1c2b39c51789f242ff0dffde39073e161deb379c79184d787d063"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "91cb8c332c643bd8b1d0a8f3ec0acd4770b407991f6de1fd320d675f2b2e95ec"
|
||||
sha256 cellar: :any_skip_relocation, arm64_monterey: "9d285fcfb55ce8ed787d1b01966d51e6e07f7e77c44a204695a2d6eee9c8698d"
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "743a282b475e87a4eaf11e545f761aef1b8e4bfe49eaee47251d7629a35a8ced"
|
||||
sha256 cellar: :any_skip_relocation, monterey: "5d63ea4f47b2028b2ba68abe12a4176934193e058edd869270221b41cc946c76"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "5a53221a680a35d1aa00cbadde279dbe4f562d22ed207c15bd4221cb8c3180f1"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "5feadb6d76f55d6f9681682e221008c282dccf0e46ae22a959b4bad2efde204a"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "d530ddbec49588b0d481f156d35f7e5bb7d3b6427d203f04750e55cd3eecc303"
|
||||
end
|
||||
|
||||
depends_on "python@3.9"
|
||||
depends_on "python@3.10"
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz"
|
||||
sha256 "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"
|
||||
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/e7/4e/2af0238001648ded297fb54ceb425ca26faa15b341b4fac5371d3938666e/charset-normalizer-2.0.4.tar.gz"
|
||||
sha256 "f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||
url "https://files.pythonhosted.org/packages/48/44/76b179e0d1afe6e6a91fd5661c284f60238987f3b42b676d141d01cd5b97/charset-normalizer-2.0.10.tar.gz"
|
||||
sha256 "876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"
|
||||
end
|
||||
|
||||
resource "defusedxml" do
|
||||
@ -34,13 +35,18 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz"
|
||||
sha256 "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
url "https://files.pythonhosted.org/packages/62/08/e3fc7c8161090f742f504f40b1bccbfc544d4a4e09eb774bf40aafce5436/idna-3.3.tar.gz"
|
||||
sha256 "9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
end
|
||||
|
||||
resource "multidict" do
|
||||
url "https://files.pythonhosted.org/packages/8e/7c/e12a69795b7b7d5071614af2c691c97fbf16a2a513c66ec52dd7d0a115bb/multidict-5.2.0.tar.gz"
|
||||
sha256 "0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"
|
||||
end
|
||||
|
||||
resource "Pygments" do
|
||||
url "https://files.pythonhosted.org/packages/b7/b3/5cba26637fe43500d4568d0ee7b7362de1fb29c0e158d50b4b69e9a40422/Pygments-2.10.0.tar.gz"
|
||||
sha256 "f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
|
||||
url "https://files.pythonhosted.org/packages/94/9c/cb656d06950268155f46d4f6ce25d7ffc51a0da47eadf1b164bbf23b718b/Pygments-2.11.2.tar.gz"
|
||||
sha256 "4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
@ -49,8 +55,8 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/e7/01/3569e0b535fb2e4a6c384bdbed00c55b9d78b5084e0fb7f4d0bf523d7670/requests-2.26.0.tar.gz"
|
||||
sha256 "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
url "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz"
|
||||
sha256 "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"
|
||||
end
|
||||
|
||||
resource "requests-toolbelt" do
|
||||
@ -59,8 +65,8 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/4f/5a/597ef5911cb8919efe4d86206aa8b2658616d676a7088f0825ca08bd7cb8/urllib3-1.26.6.tar.gz"
|
||||
sha256 "f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
url "https://files.pythonhosted.org/packages/b0/b1/7bbf5181f8e3258efae31702f5eab87d8a74a72a0aa78bc8c08c1466e243/urllib3-1.26.8.tar.gz"
|
||||
sha256 "0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||
end
|
||||
|
||||
def install
|
||||
@ -68,6 +74,10 @@ class Httpie < Formula
|
||||
end
|
||||
|
||||
test do
|
||||
assert_match version.to_s, shell_output("#{bin}/httpie --version")
|
||||
assert_match version.to_s, shell_output("#{bin}/https --version")
|
||||
assert_match version.to_s, shell_output("#{bin}/http --version")
|
||||
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
end
|
||||
|
@ -1,33 +0,0 @@
|
||||
# Contributor: Fabian Affolter <fabian@affolter-engineering.ch>
|
||||
# Maintainer: Fabian Affolter <fabian@affolter-engineering.ch>
|
||||
# Contributor: Daniel Isaksen <d@duniel.no>
|
||||
# Contributor: Mickaël Schoentgen <mickael@apible.io>
|
||||
pkgname=httpie
|
||||
pkgver=2.5.0
|
||||
pkgrel=0
|
||||
pkgdesc="CLI, cURL-like tool"
|
||||
url="https://httpie.org/"
|
||||
arch="noarch"
|
||||
license="BSD-3-Clause"
|
||||
depends="python3 py3-setuptools py3-requests py3-pygments py3-requests-toolbelt py3-pysocks py3-defusedxml"
|
||||
makedepends="py3-setuptools"
|
||||
checkdepends="py3-pytest py3-pytest-httpbin py3-responses"
|
||||
source="https://files.pythonhosted.org/packages/source/h/httpie/httpie-$pkgver.tar.gz"
|
||||
|
||||
# secfixes:
|
||||
# 1.0.3-r0:
|
||||
# - CVE-2019-10751
|
||||
|
||||
build() {
|
||||
python3 setup.py build
|
||||
}
|
||||
|
||||
check() {
|
||||
python3 -m pytest ./httpie ./tests
|
||||
}
|
||||
|
||||
package() {
|
||||
python3 setup.py install --prefix=/usr --root="$pkgdir"
|
||||
}
|
||||
|
||||
sha512sums="3bfe572b03bfde87d5a02f9ba238f9493b32e587c33fd30600a4dd6a45082eedcb2b507c7f1e3e75a423cbdcc1ff0556138897dffb7888d191834994eae9a2aa httpie-2.5.0.tar.gz"
|
@ -1,67 +0,0 @@
|
||||
# HTTPie on Alpine Linux
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Alpine 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 Alpine Linux, then you can find them on [that page](https://httpie.io/docs#alpine-linux).
|
||||
- If you are looking for technical information about the HTTPie packaging on Alpine 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 Alpine Linux.
|
||||
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://gitlab.alpinelinux.org/alpine/aports/-/blob/master/community/httpie/APKBUILD) ([example](https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/25075)).
|
||||
|
||||
Notes:
|
||||
|
||||
- The `pkgrel` value must be set to `0`.
|
||||
- The commit message must be `community/httpie: upgrade to XXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull alpine
|
||||
docker run -it --rm alpine
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Install tools
|
||||
apk add alpine-sdk sudo
|
||||
|
||||
# Add a user (password required)
|
||||
adduser me
|
||||
addgroup me abuild
|
||||
echo "me ALL=(ALL) ALL" >> /etc/sudoers
|
||||
|
||||
# Switch user
|
||||
su - me
|
||||
|
||||
# Create a private key (not used but required)
|
||||
abuild-keygen -a -i
|
||||
|
||||
# Clone
|
||||
git clone --depth=1 https://gitlab.alpinelinux.org/alpine/aports.git
|
||||
cd aports/community/httpie
|
||||
|
||||
# Retrieve the patch of the latest HTTPie version
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/linux-alpine/APKBUILD \
|
||||
-o APKBUILD
|
||||
|
||||
# Build the package
|
||||
abuild -r
|
||||
|
||||
# Install the package
|
||||
sudo apk add --repository ~/packages/community httpie
|
||||
|
||||
# And test it!
|
||||
http --version
|
||||
https --version
|
||||
```
|
@ -1,24 +0,0 @@
|
||||
# HTTPie on AOSC OS
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for AOSC OS**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for technical information about the HTTPie packaging on AOSC OS, 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 AOSC OS.
|
||||
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/AOSC-Dev/aosc-os-abbs/blob/stable/extra-web/httpie/spec) ([example](https://github.com/AOSC-Dev/aosc-os-abbs/commit/d0d3ba0bcea347387bb582a1b0b1b4e518720c80)).
|
||||
|
||||
Notes:
|
||||
|
||||
- The commit message must be `httpie: update to XXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
@ -1,5 +0,0 @@
|
||||
VER=2.5.0
|
||||
SRCS="tbl::https://github.com/httpie/httpie/archive/$VER.tar.gz"
|
||||
CHKSUMS="sha256::66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea"
|
||||
REL=1
|
||||
CHKUPDATE="anitya::id=1337"
|
@ -4,7 +4,7 @@
|
||||
# Contributor: Thomas Weißschuh <thomas_weissschuh lavabit com>
|
||||
|
||||
pkgname=httpie
|
||||
pkgver=2.5.0
|
||||
pkgver=2.6.0
|
||||
pkgrel=1
|
||||
pkgdesc="human-friendly CLI HTTP client for the API era"
|
||||
url="https://github.com/httpie/httpie"
|
||||
@ -12,7 +12,8 @@ depends=('python-defusedxml'
|
||||
'python-pygments'
|
||||
'python-pysocks'
|
||||
'python-requests'
|
||||
'python-requests-toolbelt')
|
||||
'python-requests-toolbelt'
|
||||
'python-charset-normalizer')
|
||||
makedepends=('python-setuptools')
|
||||
checkdepends=('python-pytest'
|
||||
'python-pytest-httpbin'
|
||||
@ -22,7 +23,7 @@ replaces=(python-httpie python2-httpie)
|
||||
license=('BSD')
|
||||
arch=('any')
|
||||
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
|
||||
sha256sums=('66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea')
|
||||
sha256sums=('3bcd9a8cb2b11299da12d3af36c095c6d4b665e41c395898a07f1ae4d99fc14a')
|
||||
|
||||
build() {
|
||||
cd $pkgname-$pkgver
|
||||
@ -42,5 +43,5 @@ package() {
|
||||
|
||||
check() {
|
||||
cd $pkgname-$pkgver
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 setup.py test
|
||||
PYTHONDONTWRITEBYTECODE=1 pytest tests
|
||||
}
|
||||
|
@ -19,11 +19,11 @@ 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 ([example](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=993937)):
|
||||
- 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: wishlist
|
||||
Severity: normal
|
||||
|
||||
<MESSAGE>
|
||||
```
|
||||
|
@ -42,7 +42,7 @@ 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 35) is created.
|
||||
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 34, 33), if so, merges the changes there.
|
||||
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.
|
@ -1,2 +0,0 @@
|
||||
DIST httpie-2.4.0.tar.gz 1772537 BLAKE2B 111451cc7dc353d5b586554f98ac715a3198f03e74d261944a5f021d2dcc948455500800222b323d182a2a067d0549bda7c318ab3a6c934b9a9beec64aff2db2 SHA512 44cc7ff4fe0f3d8c53a7dd750465f6b56c36f5bbac06d22b760579bd60949039e82313845699669a659ec91adc69dbeac22c06ddd63af64e6f2e0edecf3e732a
|
||||
DIST httpie-2.5.0.tar.gz 1105177 BLAKE2B 6e16868c81522d4e6d2fc0a4e093c190f18ced720b35217930865ae3f8e168193cc33dfecc13c5d310f52647d6e79d17b247f56e56e8586d633a2d9502be66a7 SHA512 f14aa23fea7578181b9bd6ededea04de9ddf0b2f697b23f76d2d96e2c17b95617318c711750bad6af550400dbc03732ab17fdf84e59d577f33f073e600a55330
|
@ -1,78 +0,0 @@
|
||||
# HTTPie on Gentoo
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Gentoo**.
|
||||
|
||||
- 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 Gentoo, then you can find them on [that page](https://httpie.io/docs#gentoo).
|
||||
- If you are looking for technical information about the HTTPie packaging on Gentoo, 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 Gentoo.
|
||||
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 create `httpie-XXX.ebuild` and update `Manifest`.
|
||||
|
||||
- Here is how to calculate the size and checksum (replace `2.5.0` with the correct version):
|
||||
|
||||
```bash
|
||||
# Download
|
||||
$ wget https://github.com/httpie/httpie/archive/2.5.0.tar.gz
|
||||
|
||||
# Size
|
||||
$ stat --printf="%s\n" 2.5.0.tar.gz
|
||||
1105177
|
||||
|
||||
# Checksum
|
||||
$ openssl dgst -blake2b512 2.5.0.tar.gz
|
||||
BLAKE2b512(2.5.0.tar.gz)= 6e16868c81522d4e6d2fc0a4e093c190f18ced720b35217930865ae3f8e168193cc33dfecc13c5d310f52647d6e79d17b247f56e56e8586d633a2d9502be66a7
|
||||
```
|
||||
|
||||
- The commit message must be `net-misc/httpie: version bump to XXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull gentoo/stage3
|
||||
docker run -it --rm gentoo/stage3
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Install tools
|
||||
emerge --sync
|
||||
emerge pkgcheck repoman
|
||||
|
||||
# Go to the package location
|
||||
cd /var/db/repos/gentoo/net-misc/httpie
|
||||
|
||||
# Retrieve the patch of the latest HTTPie version
|
||||
# (only files that were modified since the previous release)
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/linux-gentoo/httpie-XXX.ebuild \
|
||||
-o httpie-XXX.ebuild
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/linux-gentoo/Manifest \
|
||||
-o Manifest
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/linux-gentoo/metadata.xml \
|
||||
-o metadata.xml
|
||||
|
||||
# Basic checks
|
||||
repoman manifest
|
||||
repoman full -d -x
|
||||
pkgcheck scan
|
||||
|
||||
# Build and install the package
|
||||
emerge --with-test-deps httpie-XXX.ebuild
|
||||
|
||||
# Run the tests suite
|
||||
ebuild httpie-XXX.ebuild clean test
|
||||
|
||||
# And test it!
|
||||
http --version
|
||||
https --version
|
||||
```
|
@ -1,42 +0,0 @@
|
||||
# Copyright 1999-2021 Gentoo Authors
|
||||
# Distributed under the terms of the GNU General Public License v2
|
||||
|
||||
EAPI=7
|
||||
|
||||
DISTUTILS_USE_SETUPTOOLS=rdepend
|
||||
PYTHON_COMPAT=( python3_{8,9,10} )
|
||||
PYTHON_REQ_USE="ssl(+)"
|
||||
|
||||
inherit bash-completion-r1 distutils-r1
|
||||
|
||||
DESCRIPTION="Modern command line HTTP client"
|
||||
HOMEPAGE="https://httpie.io/ https://pypi.org/project/httpie/"
|
||||
SRC_URI="https://github.com/httpie/httpie/archive/${PV}.tar.gz -> ${P}.tar.gz"
|
||||
|
||||
LICENSE="BSD"
|
||||
SLOT="0"
|
||||
KEYWORDS="~amd64 ~x86"
|
||||
|
||||
RDEPEND="
|
||||
dev-python/defusedxml[${PYTHON_USEDEP}]
|
||||
dev-python/pygments[${PYTHON_USEDEP}]
|
||||
>=dev-python/requests-2.22.0[${PYTHON_USEDEP}]
|
||||
>=dev-python/requests-toolbelt-0.9.1[${PYTHON_USEDEP}]
|
||||
"
|
||||
BDEPEND="
|
||||
test? (
|
||||
${RDEPEND}
|
||||
dev-python/pyopenssl[${PYTHON_USEDEP}]
|
||||
dev-python/pytest-httpbin[${PYTHON_USEDEP}]
|
||||
dev-python/responses[${PYTHON_USEDEP}]
|
||||
)
|
||||
"
|
||||
|
||||
distutils_enable_tests pytest
|
||||
|
||||
python_install_all() {
|
||||
newbashcomp extras/httpie-completion.bash http
|
||||
insinto /usr/share/fish/vendor_completions.d
|
||||
newins extras/httpie-completion.fish http.fish
|
||||
distutils-r1_python_install_all
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE pkgmetadata SYSTEM "https://www.gentoo.org/dtd/metadata.dtd">
|
||||
<pkgmetadata>
|
||||
<maintainer type="person" proxied="yes">
|
||||
<email>mickael@apible.io</email>
|
||||
<name>Mickaël Schoentgen</name>
|
||||
</maintainer>
|
||||
<maintainer type="project" proxied="proxy">
|
||||
<email>proxy-maint@gentoo.org</email>
|
||||
<name>Proxy Maintainers</name>
|
||||
</maintainer>
|
||||
<longdescription lang="en">
|
||||
HTTPie (pronounced aitch-tee-tee-pie) is a command line HTTP
|
||||
client. Its goal is to make CLI interaction with web services as
|
||||
human-friendly as possible. It provides a simple http command
|
||||
that allows for sending arbitrary HTTP requests using a simple
|
||||
and natural syntax, and displays colorized output. HTTPie can be
|
||||
used for testing, debugging, and generally interacting with HTTP
|
||||
servers.
|
||||
</longdescription>
|
||||
<upstream>
|
||||
<bugs-to>https://github.com/httpie/httpie/issues</bugs-to>
|
||||
<changelog>https://raw.githubusercontent.com/httpie/httpie/master/CHANGELOG.md</changelog>
|
||||
<doc>https://httpie.io/docs</doc>
|
||||
<remote-id type="github">httpie/httpie</remote-id>
|
||||
<remote-id type="pypi">httpie</remote-id>
|
||||
</upstream>
|
||||
</pkgmetadata>
|
@ -1,68 +0,0 @@
|
||||
# HTTPie on Void Linux
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Void 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 Void Linux, then you can find them on [that page](https://httpie.io/docs#void-linux).
|
||||
- If you are looking for technical information about the HTTPie packaging on Void 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 Void Linux.
|
||||
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/void-linux/void-packages/blob/master/srcpkgs/httpie/template) ([example](https://github.com/void-linux/void-packages/pull/32905)).
|
||||
|
||||
- The commit message must be `httpie: update to XXX.`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull voidlinux/voidlinux
|
||||
docker run -it --rm voidlinux/voidlinux
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Sync and upgrade once, assume error comes from xbps update
|
||||
xbps-install -Syu
|
||||
# Install tools
|
||||
xbps-install -y git xtools file util-linux binutils bsdtar coreutils
|
||||
|
||||
# Clone
|
||||
git clone --depth=1 git://github.com/void-linux/void-packages.git void-packages-src
|
||||
cd void-packages-src
|
||||
|
||||
# Retrieve the patch of the latest HTTPie version
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/linux-void/template \
|
||||
-o srcpkgs/httpie/template
|
||||
|
||||
# Check the package
|
||||
xlint srcpkgs/httpie/template
|
||||
|
||||
# Link / to /masterdir
|
||||
ln -s / masterdir
|
||||
|
||||
# Enable ethereal chroot-style
|
||||
export XBPS_ALLOW_CHROOT_BREAKOUT=yes
|
||||
./xbps-src binary-bootstrap
|
||||
./xbps-src chroot
|
||||
|
||||
# Build the package
|
||||
cd void-packages
|
||||
export SOURCE_DATE_EPOCH=0
|
||||
./xbps-src pkg httpie
|
||||
|
||||
# Install the package
|
||||
xbps-install --repository=hostdir/binpkgs httpie
|
||||
|
||||
# And finally test it!
|
||||
http --version
|
||||
https --version
|
||||
```
|
@ -1,28 +0,0 @@
|
||||
# Template file for 'httpie'
|
||||
pkgname=httpie
|
||||
version=2.5.0
|
||||
revision=1
|
||||
build_style=python3-module
|
||||
hostmakedepends="python3-setuptools"
|
||||
depends="python3-setuptools python3-requests python3-requests-toolbelt
|
||||
python3-Pygments python3-pysocks python3-defusedxml"
|
||||
short_desc="Human-friendly command line HTTP client"
|
||||
maintainer="Mickaël Schoentgen <mickael@apible.io>"
|
||||
license="BSD-3-Clause"
|
||||
homepage="https://httpie.io/"
|
||||
changelog="https://raw.githubusercontent.com/httpie/httpie/${version}/CHANGELOG.md"
|
||||
distfiles="https://github.com/httpie/httpie/archive/${version}.tar.gz"
|
||||
checksum=66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea
|
||||
make_check=no # needs pytest_httpbin which is not packaged
|
||||
|
||||
post_install() {
|
||||
vcompletion extras/httpie-completion.bash bash http
|
||||
vcompletion extras/httpie-completion.fish fish http
|
||||
vlicense LICENSE
|
||||
}
|
||||
|
||||
python3-httpie_package() {
|
||||
build_style=meta
|
||||
short_desc+=" (transitional dummy package)"
|
||||
depends="httpie>=${version}_${revision}"
|
||||
}
|
@ -4,11 +4,11 @@ PortSystem 1.0
|
||||
PortGroup github 1.0
|
||||
PortGroup python 1.0
|
||||
|
||||
github.setup httpie httpie 2.5.0
|
||||
github.setup httpie httpie 2.6.0
|
||||
|
||||
maintainers {g5pw @g5pw} openmaintainer
|
||||
categories net
|
||||
description HTTPie is a command line HTTP client, a user-friendly cURL replacement.
|
||||
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 \
|
||||
@ -20,17 +20,17 @@ platforms darwin
|
||||
license BSD
|
||||
homepage https://httpie.io/
|
||||
|
||||
variant python36 conflicts python37 python38 python39 description "Use Python 3.6" {}
|
||||
variant python37 conflicts python36 python38 python39 description "Use Python 3.7" {}
|
||||
variant python38 conflicts python36 python37 python39 description "Use Python 3.8" {}
|
||||
variant python39 conflicts python36 python37 python38 description "Use Python 3.9" {}
|
||||
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 python36]} {
|
||||
python.default_version 36
|
||||
} elseif {[variant_isset python37]} {
|
||||
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
|
||||
@ -40,10 +40,11 @@ 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 88d227d52199c232c0ddf704a219d1781b1e77ee \
|
||||
sha256 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc \
|
||||
size 1105185
|
||||
checksums rmd160 07b1d1592da1c505ed3ee4ef3b6056215e16e9ff \
|
||||
sha256 63cf104bf3552305c68a74f16494a90172b15296610a875e17918e5e36373c0b \
|
||||
size 1133491
|
||||
|
||||
python.link_binaries_suffix
|
||||
|
@ -13,7 +13,7 @@ We will discuss setting up the environment, installing development tools, instal
|
||||
|
||||
## 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/12167)).
|
||||
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):
|
||||
|
||||
|
@ -1,54 +0,0 @@
|
||||
# HTTPie on Spack
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Spack**.
|
||||
|
||||
- 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 Spack, then you can find them on [that page](https://httpie.io/docs#spack-linux) ([that one](https://httpie.io/docs#spack-macos) for macOS).
|
||||
- If you are looking for technical information about the HTTPie packaging on Spack, 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 Spack. They apply to Spack on Linux, and macOS.
|
||||
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/spack/spack/blob/develop/var/spack/repos/builtin/packages/httpie/package.py) ([example](https://github.com/spack/spack/pull/25888)).
|
||||
|
||||
- The commit message must be `httpie: add vXXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull spack/centos7
|
||||
docker run -it --rm spack/centos7
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone --depth=1 https://github.com/spack/spack.git
|
||||
cd spack
|
||||
|
||||
# Retrieve the patch of the latest HTTPie version
|
||||
curl https://raw.githubusercontent.com/httpie/httpie/master/docs/packaging/spack/package.py \
|
||||
-o var/spack/repos/builtin/packages/httpie/package.py
|
||||
|
||||
# Check the package
|
||||
spack spec httpie
|
||||
|
||||
# Check available versions (it should show the new version)
|
||||
spack versions httpie
|
||||
|
||||
# Install the package
|
||||
spack install httpie@XXX
|
||||
spack load httpie
|
||||
|
||||
# And test it!
|
||||
http --version
|
||||
https --version
|
||||
```
|
@ -1,32 +0,0 @@
|
||||
# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
|
||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
from spack import *
|
||||
|
||||
|
||||
class Httpie(PythonPackage):
|
||||
"""Modern command line HTTP client."""
|
||||
|
||||
homepage = "https://httpie.io/"
|
||||
pypi = "httpie/httpie-2.5.0.tar.gz"
|
||||
|
||||
version('2.5.0', sha256='fe6a8bc50fb0635a84ebe1296a732e39357c3e1354541bf51a7057b4877e47f9')
|
||||
version('0.9.9', sha256='f1202e6fa60367e2265284a53f35bfa5917119592c2ab08277efc7fffd744fcb')
|
||||
version('0.9.8', sha256='515870b15231530f56fe2164190581748e8799b66ef0fe36ec9da3396f0df6e1')
|
||||
|
||||
variant('socks', default=True,
|
||||
description='Enable SOCKS proxy support')
|
||||
|
||||
depends_on('py-setuptools', type=('build', 'run'))
|
||||
depends_on('py-defusedxml', type=('build', 'run'))
|
||||
depends_on('py-pygments', type=('build', 'run'))
|
||||
depends_on('py-requests', type=('build', 'run'))
|
||||
depends_on('py-requests-toolbelt', type=('build', 'run'))
|
||||
depends_on('py-pysocks', type=('build', 'run'), when="+socks")
|
||||
# Concretization problem breaks this. Unconditional for now...
|
||||
# https://github.com/spack/spack/issues/3628
|
||||
# depends_on('py-argparse@1.2.1:', type=('build', 'run'),
|
||||
# when='^python@:2.6,3.0:3.1')
|
||||
depends_on('py-argparse@1.2.1:', type=('build', 'run'), when='^python@:2.6')
|
@ -2,8 +2,8 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>httpie</id>
|
||||
<version>2.5.0</version>
|
||||
<summary>Modern, user-friendly command-line HTTP client for the API era.</summary>
|
||||
<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.
|
||||
@ -28,20 +28,20 @@ Main features:
|
||||
</description>
|
||||
<title>HTTPie</title>
|
||||
<authors>HTTPie</authors>
|
||||
<owners>Tiger-222</owners>
|
||||
<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.5.0/CHANGELOG.md).</releaseNotes>
|
||||
<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</packageSourceUrl>
|
||||
<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.6" />
|
||||
<dependency id="python3" version="3.7" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
|
@ -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
|
||||
|
39
extras/profiling/README.md
Normal file
39
extras/profiling/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# HTTPie Benchmarking Infrastructure
|
||||
|
||||
This directory includes the benchmarks we use for testing HTTPie's speed and the
|
||||
infrastructure to automate this testing accross versions.
|
||||
|
||||
## Usage
|
||||
|
||||
Ensure the following requirements are satisfied:
|
||||
|
||||
- Python 3.7+
|
||||
- `pyperf`
|
||||
|
||||
Then, run the `extras/benchmarks/run.py`:
|
||||
|
||||
```console
|
||||
$ python extras/profiling/run.py
|
||||
```
|
||||
|
||||
Without any options, this command will initially create an isolated environment
|
||||
and install `httpie` from the latest commit. Then it will create a second
|
||||
environment with the `master` of the current repository and run the benchmarks
|
||||
on both of them. It will compare the results and print it as a markdown table:
|
||||
|
||||
| Benchmark | master | this_branch |
|
||||
| -------------------------------------- | :----: | :------------------: |
|
||||
| `http --version` (startup) | 201 ms | 174 ms: 1.16x faster |
|
||||
| `http --offline pie.dev/get` (startup) | 200 ms | 174 ms: 1.15x faster |
|
||||
| Geometric mean | (ref) | 1.10x faster |
|
||||
|
||||
If your `master` branch is not up-to-date, you can get a fresh clone by passing
|
||||
`--fresh` option. This way, the benchmark runner will clone the `httpie/httpie`
|
||||
repo from `GitHub` and use it as the baseline.
|
||||
|
||||
You can customize these branches by passing `--local-repo`/`--target-branch`,
|
||||
and customize the repos by passing `--local-repo`/`--target-repo` (can either
|
||||
take a URL or a path).
|
||||
|
||||
If you want to run a third enviroment with additional dependencies (such as
|
||||
`pyOpenSSL`), you can pass `--complex`.
|
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.6.0'
|
||||
__version__ = '3.0.2'
|
||||
__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,
|
||||
@ -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:
|
||||
|
@ -57,7 +57,7 @@ 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)
|
||||
|
||||
@ -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 = ['']
|
||||
|
@ -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 = '^'
|
||||
|
@ -12,20 +12,21 @@ from .argtypes import (
|
||||
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(
|
||||
@ -41,6 +42,7 @@ parser = HTTPieArgumentParser(
|
||||
|
||||
'''),
|
||||
)
|
||||
parser.register('action', 'lazy_choices', LazyChoices)
|
||||
|
||||
#######################################################################
|
||||
# Positional arguments.
|
||||
@ -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=sorted(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,
|
||||
@ -375,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.
|
||||
@ -393,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',
|
||||
@ -407,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(
|
||||
@ -554,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,
|
||||
@ -596,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',
|
||||
|
@ -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_and_dupe_keys
|
||||
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:
|
||||
|
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,22 @@ import http.client
|
||||
import json
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, Union
|
||||
from time import monotonic
|
||||
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 .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 +35,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 +51,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 +83,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,
|
||||
@ -104,7 +109,7 @@ def collect_messages(
|
||||
**send_kwargs_merged,
|
||||
**send_kwargs,
|
||||
)
|
||||
|
||||
response._httpie_headers_parsed_at = monotonic()
|
||||
expired_cookies += get_expired_cookies(
|
||||
response.headers.get('Set-Cookie', '')
|
||||
)
|
||||
@ -152,6 +157,7 @@ def build_requests_session(
|
||||
requests_session = requests.Session()
|
||||
|
||||
# Install our adapter.
|
||||
http_adapter = HTTPieHTTPAdapter()
|
||||
https_adapter = HTTPieHTTPSAdapter(
|
||||
ciphers=ciphers,
|
||||
verify=verify,
|
||||
@ -160,6 +166,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 +185,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 +197,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 +275,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 +308,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 +333,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,5 @@
|
||||
import sys
|
||||
from typing import Any, Optional, Iterable
|
||||
|
||||
|
||||
is_windows = 'win32' in str(sys.platform).lower()
|
||||
@ -52,3 +53,38 @@ except ImportError:
|
||||
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')
|
||||
|
@ -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,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:
|
||||
@ -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')
|
||||
|
142
httpie/core.py
142
httpie/core.py
@ -2,46 +2,63 @@ 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(),
|
||||
use_default_options: bool = True,
|
||||
) -> ExitStatus:
|
||||
program_name, *args = args
|
||||
env.program_name = os.path.basename(program_name)
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
plugin_manager.load_installed_plugins()
|
||||
plugin_manager.load_installed_plugins(env.config.plugins_dir)
|
||||
|
||||
from .cli.definition import parser
|
||||
|
||||
if env.config.default_options:
|
||||
if use_default_options and env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
|
||||
include_debug_info = '--debug' in args
|
||||
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 +71,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 +89,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 +113,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 +187,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
|
||||
@ -187,9 +222,10 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
||||
write_message(requests_message=message, env=env, args=args, 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:
|
||||
|
@ -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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from typing import Union, Tuple
|
||||
|
||||
from charset_normalizer import from_bytes
|
||||
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
|
||||
@ -29,7 +29,7 @@ def detect_encoding(content: ContentBytes) -> str:
|
||||
return encoding
|
||||
|
||||
|
||||
def smart_decode(content: ContentBytes, encoding: str) -> str:
|
||||
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`.
|
||||
|
||||
@ -38,7 +38,7 @@ def smart_decode(content: ContentBytes, encoding: str) -> str:
|
||||
"""
|
||||
if not encoding:
|
||||
encoding = detect_encoding(content)
|
||||
return content.decode(encoding, 'replace')
|
||||
return content.decode(encoding, 'replace'), encoding
|
||||
|
||||
|
||||
def smart_encode(content: str, encoding: str) -> bytes:
|
||||
|
0
httpie/manager/__init__.py
Normal file
0
httpie/manager/__init__.py
Normal file
62
httpie/manager/__main__.py
Normal file
62
httpie/manager/__main__.py
Normal file
@ -0,0 +1,62 @@
|
||||
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,
|
||||
use_default_options=False,
|
||||
)
|
||||
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
|
109
httpie/models.py
109
httpie/models.py
@ -1,8 +1,23 @@
|
||||
from typing import Iterable
|
||||
from time import monotonic
|
||||
|
||||
import requests
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Iterable, Union, NamedTuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .utils import split_cookies, parse_content_type_header
|
||||
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
|
||||
|
||||
|
||||
ELAPSED_TIME_LABEL = 'Elapsed time'
|
||||
|
||||
|
||||
class HTTPMessage:
|
||||
@ -24,6 +39,11 @@ class HTTPMessage:
|
||||
"""Return a `str` with the message's headers."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def metadata(self) -> str:
|
||||
"""Return metadata about the current message."""
|
||||
raise NotImplementedError
|
||||
|
||||
@cached_property
|
||||
def encoding(self) -> str:
|
||||
ct, params = parse_content_type_header(self.content_type)
|
||||
@ -72,10 +92,27 @@ 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 metadata(self) -> str:
|
||||
data = {}
|
||||
time_to_parse_headers = self._orig.elapsed.total_seconds()
|
||||
# noinspection PyProtectedMember
|
||||
time_since_headers_parsed = monotonic() - self._orig._httpie_headers_parsed_at
|
||||
time_elapsed = time_to_parse_headers + time_since_headers_parsed
|
||||
# data['Headers time'] = str(round(time_to_parse_headers, 5)) + 's'
|
||||
# data['Body time'] = str(round(time_since_headers_parsed, 5)) + 's'
|
||||
data[ELAPSED_TIME_LABEL] = str(round(time_elapsed, 10)) + 's'
|
||||
return '\n'.join(
|
||||
f'{key}: {value}'
|
||||
for key, value in data.items()
|
||||
)
|
||||
|
||||
|
||||
class HTTPRequest(HTTPMessage):
|
||||
"""A :class:`requests.models.Request` wrapper."""
|
||||
@ -96,7 +133,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]
|
||||
|
||||
@ -116,3 +153,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
|
||||
@ -15,7 +16,8 @@ from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from ..lexers.json import EnhancedJsonLexer
|
||||
from ...compat import is_windows
|
||||
from ..lexers.metadata import MetadataLexer
|
||||
from ..ui.palette import SHADE_NAMES, get_color
|
||||
from ...context import Environment
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
@ -23,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):
|
||||
@ -42,6 +45,7 @@ class ColorFormatter(FormatterPlugin):
|
||||
|
||||
"""
|
||||
group_name = 'colors'
|
||||
metadata_lexer = MetadataLexer()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -60,23 +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:
|
||||
from ..lexers.http import SimplifiedHTTPLexer
|
||||
http_lexer = SimplifiedHTTPLexer()
|
||||
formatter = Terminal256Formatter(
|
||||
style=self.get_style_class(color_scheme)
|
||||
)
|
||||
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:
|
||||
@ -85,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
|
||||
@ -99,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:
|
||||
@ -232,3 +263,125 @@ 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()
|
||||
PIE_STYLE_NAMES = list(PIE_STYLES.keys())
|
||||
BUNDLED_STYLES |= PIE_STYLES.keys()
|
||||
|
@ -1,4 +1,3 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ...encoding import UTF8
|
||||
@ -8,27 +7,47 @@ 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(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
|
||||
|
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
|
@ -1,4 +1,54 @@
|
||||
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):
|
||||
@ -18,7 +68,7 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Name.Function,
|
||||
request_method,
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Namespace,
|
||||
pygments.token.Text,
|
||||
@ -27,15 +77,13 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
pygments.token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(.+)',
|
||||
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
|
||||
http_response_type, # Status code and Reason
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
|
59
httpie/output/lexers/metadata.py
Normal file
59
httpie/output/lexers/metadata.py
Normal file
@ -0,0 +1,59 @@
|
||||
import pygments
|
||||
|
||||
from httpie.models import ELAPSED_TIME_LABEL
|
||||
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': [
|
||||
(
|
||||
fr'({ELAPSED_TIME_LABEL})( *)(:)( *)(\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 .processing import Conversion, Formatting
|
||||
from ..context import Environment
|
||||
from ..encoding import smart_decode, smart_encode, UTF8
|
||||
from ..models import HTTPMessage
|
||||
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.
|
||||
|
||||
@ -106,8 +118,12 @@ class EncodedStream(BaseStream):
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.mime = mime_overwrite or self.msg.content_type
|
||||
self.encoding = encoding_overwrite or self.msg.encoding
|
||||
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
|
||||
@ -121,9 +137,33 @@ class EncodedStream(BaseStream):
|
||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||
if b'\0' in line:
|
||||
raise BinarySuppressedError()
|
||||
line = smart_decode(line, self.encoding)
|
||||
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):
|
||||
"""In addition to :class:`EncodedStream` behaviour, this stream applies
|
||||
@ -149,6 +189,10 @@ class PrettyStream(EncodedStream):
|
||||
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)
|
||||
@ -174,7 +218,7 @@ class PrettyStream(EncodedStream):
|
||||
if not isinstance(chunk, str):
|
||||
# Text when a converter has been used,
|
||||
# otherwise it will always be bytes.
|
||||
chunk = smart_decode(chunk, self.encoding)
|
||||
chunk = self.decode_chunk(chunk)
|
||||
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
|
||||
return smart_encode(chunk, self.output_encoding)
|
||||
|
||||
|
0
httpie/output/ui/__init__.py
Normal file
0
httpie/output/ui/__init__.py
Normal file
167
httpie/output/ui/palette.py
Normal file
167
httpie/output/ui/palette.py
Normal file
@ -0,0 +1,167 @@
|
||||
from typing import Optional
|
||||
|
||||
STYLE_PIE = 'pie'
|
||||
STYLE_PIE_DARK = 'pie-dark'
|
||||
STYLE_PIE_LIGHT = 'pie-light'
|
||||
|
||||
|
||||
COLOR_PALETTE = {
|
||||
# Copy the brand 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': STYLE_PIE_DARK,
|
||||
'600': STYLE_PIE,
|
||||
'700': STYLE_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
|
@ -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, HTTPMessage
|
||||
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,26 +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 = {
|
||||
requests.PreparedRequest: HTTPRequest,
|
||||
requests.Response: HTTPResponse,
|
||||
}[type(requests_message)]
|
||||
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
|
||||
)
|
||||
yield from stream_class(
|
||||
msg=message_type(requests_message),
|
||||
with_headers=with_headers,
|
||||
with_body=with_body,
|
||||
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.
|
||||
@ -123,16 +126,23 @@ def get_stream_type_and_kwargs(
|
||||
env: Environment,
|
||||
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
|
||||
)
|
||||
}
|
||||
@ -147,7 +157,7 @@ def get_stream_type_and_kwargs(
|
||||
'encoding_overwrite': args.response_charset,
|
||||
})
|
||||
if args.prettify:
|
||||
stream_class = PrettyStream if args.stream else BufferedPrettyStream
|
||||
stream_class = PrettyStream if is_stream else BufferedPrettyStream
|
||||
stream_kwargs.update({
|
||||
'conversion': Conversion(),
|
||||
'formatting': Formatting(
|
||||
|
@ -155,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,24 +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 ..utils import repr_dict
|
||||
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)
|
||||
@ -29,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):
|
||||
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:
|
||||
for entry_point in iter_entry_points(entry_point_name):
|
||||
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]]:
|
||||
|
@ -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
|
||||
|
||||
@ -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:
|
||||
|
@ -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
|
||||
|
||||
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,38 +46,82 @@ class ChunkedMultipartUploadStream:
|
||||
yield chunk
|
||||
|
||||
|
||||
def prepare_request_body(
|
||||
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
|
||||
body_read_callback: Callable[[bytes], bytes],
|
||||
content_length_header_value: int = None,
|
||||
chunked=False,
|
||||
offline=False,
|
||||
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
|
||||
|
||||
is_file_like = hasattr(body, 'read')
|
||||
|
||||
if isinstance(body, RequestDataDict):
|
||||
body = urlencode(body, doseq=True)
|
||||
|
||||
if offline:
|
||||
if is_file_like:
|
||||
return body.read()
|
||||
return body
|
||||
|
||||
if not is_file_like:
|
||||
if chunked:
|
||||
body = ChunkedUploadStream(
|
||||
# Pass the entire body as one chunk.
|
||||
stream=(chunk.encode() for chunk in [body]),
|
||||
callback=body_read_callback,
|
||||
)
|
||||
def as_bytes(data: Union[str, bytes]) -> bytes:
|
||||
if isinstance(data, str):
|
||||
return data.encode()
|
||||
else:
|
||||
# File-like object.
|
||||
return data
|
||||
|
||||
if not super_len(body):
|
||||
|
||||
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
|
||||
@ -73,28 +129,64 @@ def prepare_request_body(
|
||||
# something like --no-chunked.
|
||||
# This would be backwards-incompatible so wait until v3.0.0.
|
||||
#
|
||||
body = body.read()
|
||||
file = as_bytes(file.read())
|
||||
else:
|
||||
orig_read = body.read
|
||||
|
||||
def new_read(*args):
|
||||
chunk = orig_read(*args)
|
||||
body_read_callback(chunk)
|
||||
return chunk
|
||||
|
||||
body.read = new_read
|
||||
file.read = _wrap_function_with_callback(
|
||||
file.read,
|
||||
callback
|
||||
)
|
||||
|
||||
if chunked:
|
||||
if isinstance(body, MultipartEncoder):
|
||||
body = ChunkedMultipartUploadStream(
|
||||
encoder=body,
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
if isinstance(file, MultipartEncoder):
|
||||
return ChunkedMultipartUploadStream(
|
||||
encoder=file,
|
||||
)
|
||||
else:
|
||||
body = ChunkedUploadStream(
|
||||
stream=body,
|
||||
callback=body_read_callback,
|
||||
return ChunkedUploadStream(
|
||||
stream=file,
|
||||
callback=callback,
|
||||
)
|
||||
else:
|
||||
return file
|
||||
|
||||
|
||||
def prepare_request_body(
|
||||
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 as_bytes(raw_body.read())
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
@ -102,7 +194,9 @@ 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,
|
||||
|
@ -3,16 +3,20 @@ import mimetypes
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import sysconfig
|
||||
|
||||
from collections import OrderedDict
|
||||
from http.cookiejar import parse_ns_headers
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Any, List, Optional, Tuple
|
||||
from typing import Any, List, Optional, Tuple, Callable, Iterable, TypeVar
|
||||
|
||||
import requests.auth
|
||||
|
||||
RE_COOKIE_SPLIT = re.compile(r', (?=[^ ;]+=)')
|
||||
Item = Tuple[str, Any]
|
||||
Items = List[Item]
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class JsonDictPreservingDuplicateKeys(OrderedDict):
|
||||
@ -207,3 +211,29 @@ def parse_content_type_header(header):
|
||||
value = param[index_of_equals + 1:].strip(items_to_strip)
|
||||
params_dict[key.lower()] = value
|
||||
return content_type, params_dict
|
||||
|
||||
|
||||
def as_site(path: Path) -> Path:
|
||||
site_packages_path = sysconfig.get_path(
|
||||
'purelib',
|
||||
vars={'base': str(path)}
|
||||
)
|
||||
return Path(site_packages_path)
|
||||
|
||||
|
||||
def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
|
||||
left, right = [], []
|
||||
for item in iterable:
|
||||
if key(item):
|
||||
left.append(item)
|
||||
else:
|
||||
right.append(item)
|
||||
return left, right
|
||||
|
||||
|
||||
def unwrap_context(exc: Exception) -> Optional[Exception]:
|
||||
context = exc.__context__
|
||||
if isinstance(context, Exception):
|
||||
return unwrap_context(context)
|
||||
else:
|
||||
return exc
|
||||
|
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
markers =
|
||||
requires_installation
|
12
setup.py
12
setup.py
@ -33,7 +33,9 @@ install_requires = [
|
||||
'requests[socks]>=2.22.0',
|
||||
'Pygments>=2.5.2',
|
||||
'requests-toolbelt>=0.9.1',
|
||||
'multidict>=4.7.0',
|
||||
'setuptools',
|
||||
'importlib-metadata>=1.4.0; python_version < "3.8"',
|
||||
]
|
||||
install_requires_win_only = [
|
||||
'colorama>=0.2.4',
|
||||
@ -69,7 +71,7 @@ setup(
|
||||
description=httpie.__doc__.strip(),
|
||||
long_description=long_description(),
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://httpie.org/',
|
||||
url='https://httpie.io/',
|
||||
download_url=f'https://github.com/httpie/httpie/archive/{httpie.__version__}.tar.gz',
|
||||
author=httpie.__author__,
|
||||
author_email='jakub@roztocil.co',
|
||||
@ -79,9 +81,10 @@ setup(
|
||||
'console_scripts': [
|
||||
'http = httpie.__main__:main',
|
||||
'https = httpie.__main__:main',
|
||||
'httpie = httpie.manager.__main__:main',
|
||||
],
|
||||
},
|
||||
python_requires='>=3.6',
|
||||
python_requires='>=3.7',
|
||||
extras_require=extras_require,
|
||||
install_requires=install_requires,
|
||||
classifiers=[
|
||||
@ -102,7 +105,8 @@ setup(
|
||||
project_urls={
|
||||
'GitHub': 'https://github.com/httpie/httpie',
|
||||
'Twitter': 'https://twitter.com/httpie',
|
||||
'Documentation': 'https://httpie.org/docs',
|
||||
'Online Demo': 'https://httpie.org/run',
|
||||
'Discord': 'https://httpie.io/discord',
|
||||
'Documentation': 'https://httpie.io/docs',
|
||||
'Online Demo': 'https://httpie.io/run',
|
||||
},
|
||||
)
|
||||
|
@ -64,24 +64,19 @@ parts:
|
||||
python -m pip install httpie-unixsocket
|
||||
python -m pip install httpie-snapdsocket
|
||||
|
||||
echo "Removing no more needed modules ..."
|
||||
python -m pip uninstall -y pip wheel
|
||||
|
||||
override-prime: |
|
||||
snapcraftctl prime
|
||||
|
||||
echo "Removing useless files ..."
|
||||
packages=$SNAPCRAFT_PRIME/lib/python3.8/site-packages
|
||||
rm -rfv $packages/_distutils_hack
|
||||
rm -rfv $packages/pkg_resources/tests
|
||||
rm -rfv $packages/requests_unixsocket/test*
|
||||
rm -rfv $packages/setuptools
|
||||
|
||||
echo "Compiling pyc files ..."
|
||||
python -m compileall -f $packages
|
||||
|
||||
echo "Copying extra files ..."
|
||||
cp $SNAPCRAFT_PART_SRC/extras/httpie-completion.bash $SNAPCRAFT_PRIME/bin/
|
||||
cp $SNAPCRAFT_PART_SRC/extras/httpie-completion.bash $SNAPCRAFT_PRIME/
|
||||
|
||||
plugs:
|
||||
dot-config-httpie:
|
||||
@ -102,13 +97,19 @@ apps:
|
||||
- home
|
||||
- network
|
||||
- removable-media
|
||||
completer: bin/httpie-completion.bash
|
||||
completer: httpie-completion.bash
|
||||
environment:
|
||||
LC_ALL: C.UTF-8
|
||||
|
||||
https:
|
||||
command: bin/https
|
||||
plugs: *plugs
|
||||
completer: bin/httpie-completion.bash
|
||||
completer: httpie-completion.bash
|
||||
environment:
|
||||
LC_ALL: C.UTF-8
|
||||
|
||||
httpie:
|
||||
command: bin/httpie
|
||||
plugs: *plugs
|
||||
environment:
|
||||
LC_ALL: C.UTF-8
|
||||
|
@ -5,6 +5,15 @@ import pytest
|
||||
from pytest_httpbin import certs
|
||||
|
||||
from .utils import HTTPBIN_WITH_CHUNKED_SUPPORT_DOMAIN, HTTPBIN_WITH_CHUNKED_SUPPORT
|
||||
from .utils.plugins_cli import ( # noqa
|
||||
broken_plugin,
|
||||
dummy_plugin,
|
||||
dummy_plugins,
|
||||
httpie_plugins,
|
||||
httpie_plugins_success,
|
||||
interface,
|
||||
)
|
||||
from .utils.http_server import http_server # noqa
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
|
2
tests/fixtures/__init__.py
vendored
2
tests/fixtures/__init__.py
vendored
@ -32,6 +32,8 @@ JSON_FILE_PATH_ARG = patharg(JSON_FILE_PATH)
|
||||
# line would be escaped).
|
||||
FILE_CONTENT = FILE_PATH.read_text(encoding=UTF8).strip()
|
||||
|
||||
ASCII_FILE_CONTENT = "random text" * 10
|
||||
|
||||
|
||||
JSON_FILE_CONTENT = JSON_FILE_PATH.read_text(encoding=UTF8)
|
||||
BIN_FILE_CONTENT = BIN_FILE_PATH.read_bytes()
|
||||
|
3
tests/fixtures/xmldata/valid/custom-header.xml
vendored
Normal file
3
tests/fixtures/xmldata/valid/custom-header.xml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- comment -->
|
||||
<root><element key="value">text</element><element>text</element>tail<empty-element/></root>
|
||||
<!-- comment -->
|
9
tests/fixtures/xmldata/valid/custom-header_formatted.xml
vendored
Normal file
9
tests/fixtures/xmldata/valid/custom-header_formatted.xml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- comment -->
|
||||
<root>
|
||||
<element key="value">text</element>
|
||||
<element>text</element>
|
||||
tail
|
||||
<empty-element/>
|
||||
</root>
|
||||
|
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?pi data?>
|
||||
<!-- comment -->
|
||||
<root xmlns="namespace">
|
||||
|
1
tests/fixtures/xmldata/valid/simple-single-tag_formatted.xml
vendored
Normal file
1
tests/fixtures/xmldata/valid/simple-single-tag_formatted.xml
vendored
Normal file
@ -0,0 +1 @@
|
||||
<a/>
|
1
tests/fixtures/xmldata/valid/simple-single-tag_raw.xml
vendored
Normal file
1
tests/fixtures/xmldata/valid/simple-single-tag_raw.xml
vendored
Normal file
@ -0,0 +1 @@
|
||||
<a></a>
|
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- comment -->
|
||||
<root>
|
||||
<element key="value">text</element>
|
||||
|
@ -25,6 +25,19 @@ def test_digest_auth(httpbin_both, argument_name):
|
||||
assert r.json == {'authenticated': True, 'user': 'user'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('token', [
|
||||
'token_1',
|
||||
'long_token' * 5,
|
||||
'user:style',
|
||||
])
|
||||
def test_bearer_auth(httpbin_both, token):
|
||||
r = http('--auth-type', 'bearer', '--auth', token,
|
||||
httpbin_both + '/bearer')
|
||||
|
||||
assert HTTP_OK in r
|
||||
assert r.json == {'authenticated': True, 'token': token}
|
||||
|
||||
|
||||
@mock.patch('httpie.cli.argtypes.AuthCredentials._getpass',
|
||||
new=lambda self, prompt: 'password')
|
||||
def test_password_prompt(httpbin):
|
||||
|
@ -39,8 +39,8 @@ class TestItemParsing:
|
||||
# files
|
||||
self.key_value_arg(fr'bar\@baz@{FILE_PATH_ARG}'),
|
||||
])
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(items.headers._store.values())
|
||||
# `HTTPHeadersDict` => `dict`
|
||||
headers = dict(items.headers)
|
||||
|
||||
assert headers == {
|
||||
'foo:bar': 'baz',
|
||||
@ -79,21 +79,26 @@ class TestItemParsing:
|
||||
self.key_value_arg('Empty-Header;'),
|
||||
self.key_value_arg('list:=["a", 1, {}, false]'),
|
||||
self.key_value_arg('obj:={"a": "b"}'),
|
||||
self.key_value_arg(r'nested\[2\][a][]=1'),
|
||||
self.key_value_arg('nested[2][a][]:=1'),
|
||||
self.key_value_arg('ed='),
|
||||
self.key_value_arg('bool:=true'),
|
||||
self.key_value_arg('file@' + FILE_PATH_ARG),
|
||||
self.key_value_arg('query==value'),
|
||||
self.key_value_arg('Embedded-Header:@' + FILE_PATH_ARG),
|
||||
self.key_value_arg('string-embed=@' + FILE_PATH_ARG),
|
||||
self.key_value_arg('param-embed==@' + FILE_PATH_ARG),
|
||||
self.key_value_arg('raw-json-embed:=@' + JSON_FILE_PATH_ARG),
|
||||
])
|
||||
|
||||
# Parsed headers
|
||||
# `requests.structures.CaseInsensitiveDict` => `dict`
|
||||
headers = dict(items.headers._store.values())
|
||||
# `HTTPHeadersDict` => `dict`
|
||||
headers = dict(items.headers)
|
||||
assert headers == {
|
||||
'Header': 'value',
|
||||
'Unset-Header': None,
|
||||
'Empty-Header': ''
|
||||
'Empty-Header': '',
|
||||
'Embedded-Header': FILE_CONTENT.rstrip('\n')
|
||||
}
|
||||
|
||||
# Parsed data
|
||||
@ -104,14 +109,17 @@ class TestItemParsing:
|
||||
'ed': '',
|
||||
'string': 'value',
|
||||
'bool': True,
|
||||
'list': ['a', 1, {}, False],
|
||||
'list': ['a', 1, load_json_preserve_order_and_dupe_keys('{}'), False],
|
||||
'nested[2]': {'a': ['1']},
|
||||
'nested': [None, None, {'a': [1]}],
|
||||
'obj': load_json_preserve_order_and_dupe_keys('{"a": "b"}'),
|
||||
'string-embed': FILE_CONTENT,
|
||||
'string-embed': FILE_CONTENT
|
||||
}
|
||||
|
||||
# Parsed query string parameters
|
||||
assert items.params == {
|
||||
'query': 'value'
|
||||
'query': 'value',
|
||||
'param-embed': FILE_CONTENT.rstrip('\n')
|
||||
}
|
||||
|
||||
# Parsed file fields
|
||||
@ -132,7 +140,7 @@ class TestItemParsing:
|
||||
self.key_value_arg('text_field=a'),
|
||||
self.key_value_arg('text_field=b')
|
||||
],
|
||||
as_form=True,
|
||||
request_type=constants.RequestType.FORM,
|
||||
)
|
||||
assert items.data['text_field'] == ['a', 'b']
|
||||
assert list(items.data.items()) == [
|
||||
@ -169,6 +177,21 @@ class TestQuerystring:
|
||||
assert f'"url": "{url}"' in r
|
||||
|
||||
|
||||
@pytest.mark.parametrize(['program_name', 'url_arg', 'parsed_url'], [
|
||||
('http', '://pie.dev/get', 'http://pie.dev/get'),
|
||||
('https', '://pie.dev/get', 'https://pie.dev/get'),
|
||||
])
|
||||
def test_url_leading_colon_slash_slash(program_name, url_arg, parsed_url):
|
||||
env = MockEnvironment(program_name=program_name)
|
||||
args = parser.parse_args(args=[url_arg], env=env)
|
||||
assert args.url == parsed_url
|
||||
|
||||
|
||||
def test_url_colon_slash_slash_only():
|
||||
r = http('://', tolerate_error_exit_status=True)
|
||||
assert r.stderr.strip() == "http: error: InvalidURL: Invalid URL 'http://': No host supplied"
|
||||
|
||||
|
||||
class TestLocalhostShorthand:
|
||||
def test_expand_localhost_shorthand(self):
|
||||
args = parser.parse_args(args=[':'], env=MockEnvironment())
|
||||
|
86
tests/test_cli_utils.py
Normal file
86
tests/test_cli_utils.py
Normal file
@ -0,0 +1,86 @@
|
||||
import pytest
|
||||
from argparse import ArgumentParser
|
||||
from unittest.mock import Mock
|
||||
from httpie.cli.utils import LazyChoices
|
||||
|
||||
|
||||
def test_lazy_choices():
|
||||
mock = Mock()
|
||||
getter = mock.getter
|
||||
getter.return_value = ['a', 'b', 'c']
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.register('action', 'lazy_choices', LazyChoices)
|
||||
parser.add_argument(
|
||||
'--option',
|
||||
help="the regular option",
|
||||
default='a',
|
||||
metavar='SYMBOL',
|
||||
choices=['a', 'b'],
|
||||
)
|
||||
parser.add_argument(
|
||||
'--lazy-option',
|
||||
help="the lazy option",
|
||||
default='a',
|
||||
metavar='SYMBOL',
|
||||
action='lazy_choices',
|
||||
getter=getter,
|
||||
cache=False # for test purposes
|
||||
)
|
||||
|
||||
# Parser initalization doesn't call it.
|
||||
getter.assert_not_called()
|
||||
|
||||
# If we don't use --lazy-option, we don't retrieve it.
|
||||
parser.parse_args([])
|
||||
getter.assert_not_called()
|
||||
|
||||
parser.parse_args(['--option', 'b'])
|
||||
getter.assert_not_called()
|
||||
|
||||
# If we pass a value, it will retrieve to verify.
|
||||
parser.parse_args(['--lazy-option', 'c'])
|
||||
getter.assert_called()
|
||||
getter.reset_mock()
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(['--lazy-option', 'z'])
|
||||
getter.assert_called()
|
||||
getter.reset_mock()
|
||||
|
||||
|
||||
def test_lazy_choices_help():
|
||||
mock = Mock()
|
||||
getter = mock.getter
|
||||
getter.return_value = ['a', 'b', 'c']
|
||||
|
||||
help_formatter = mock.help_formatter
|
||||
help_formatter.return_value = '<my help>'
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.register('action', 'lazy_choices', LazyChoices)
|
||||
parser.add_argument(
|
||||
'--lazy-option',
|
||||
default='a',
|
||||
metavar='SYMBOL',
|
||||
action='lazy_choices',
|
||||
getter=getter,
|
||||
help_formatter=help_formatter,
|
||||
cache=False # for test purposes
|
||||
)
|
||||
|
||||
# Parser initalization doesn't call it.
|
||||
getter.assert_not_called()
|
||||
|
||||
# If we don't use `--help`, we don't use it.
|
||||
parser.parse_args([])
|
||||
getter.assert_not_called()
|
||||
help_formatter.assert_not_called()
|
||||
|
||||
parser.parse_args(['--lazy-option', 'b'])
|
||||
help_formatter.assert_not_called()
|
||||
|
||||
# If we use --help, then we call it with styles
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(['--help'])
|
||||
help_formatter.assert_called_once_with(['a', 'b', 'c'])
|
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import requests
|
||||
from unittest import mock
|
||||
from urllib.request import urlopen
|
||||
|
||||
@ -14,7 +15,7 @@ from httpie.downloads import (
|
||||
from .utils import http, MockEnvironment
|
||||
|
||||
|
||||
class Response:
|
||||
class Response(requests.Response):
|
||||
# noinspection PyDefaultArgument
|
||||
def __init__(self, url, headers={}, status_code=200):
|
||||
self.url = url
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user