forked from extern/httpie-cli
Compare commits
114 Commits
mickael/os
...
3.0.0
Author | SHA1 | Date | |
---|---|---|---|
88140422a9 | |||
d2d40eb336 | |||
cd877a5e08 | |||
87629706c9 | |||
3856f94d3d | |||
dc30919893 | |||
fb82f44cd1 | |||
eb4e32ca28 | |||
980bd59e29 | |||
2cda966384 | |||
7bf373751d | |||
21faddc4b9 | |||
c126bc11c7 | |||
00c859c51d | |||
508788ca56 | |||
4c56d894ba | |||
0e10e23dca | |||
06512c72a3 | |||
8d84248ee3 | |||
17ed3bb8c5 | |||
05c02f0f39 | |||
0ebc9a7e09 | |||
c692669526 | |||
747accc2ae | |||
f3b500119c | |||
e0e03f3237 | |||
be87da8bbd | |||
e09401b81a | |||
5a83a9ebc4 | |||
c97ec93a19 | |||
2d15659b16 | |||
021b41c9e5 | |||
8dc6c0df77 | |||
1bd8422fb5 | |||
c237e15108 | |||
a5d8b51e47 | |||
2b78d04410 | |||
7bd7aa20d2 | |||
7ae44aefe2 | |||
28e874535a | |||
340fef6278 | |||
088b6cdb0c | |||
43462f8af0 | |||
e4b2751a52 | |||
f94c12d8ca | |||
3db1cdba4c | |||
4f7f59b990 | |||
e30ec6be42 | |||
207b970d94 | |||
62e43abc86 | |||
ea8e22677a | |||
df58ec683e | |||
8fe1f08a37 | |||
521ddde4c5 | |||
3457806df1 | |||
840f77d2a8 | |||
6522ce06d0 | |||
f927065416 | |||
151becec2b | |||
ba8e4097e8 | |||
00b366a81f | |||
5bf696d113 | |||
3081fc1a3c | |||
245cede2c2 | |||
6bdcdf1eba | |||
0fc6331ee0 | |||
ef62fc11bf | |||
c000886546 | |||
cfcd7413d1 | |||
7dfa001d2c | |||
06d9c14e7a | |||
861b8b36a8 | |||
434512e92f | |||
72735d9d59 | |||
7cdd74fece | |||
d40f06687f | |||
0d9c8b88b3 | |||
cff45276b5 | |||
e75e0a0565 | |||
19e48ba901 | |||
a9b8513f62 | |||
7985cf60c8 | |||
5dc4a26277 | |||
7775422afb | |||
2be43e698a | |||
3abc76f6d5 | |||
021eb651e0 | |||
419427cfb6 | |||
7500912be1 | |||
1b4048aefc | |||
7885f5cd66 | |||
3e414d731c | |||
d8f6a5fe52 | |||
cee283a01a | |||
5c267003c7 | |||
cdab8e67cb | |||
6c6093a46d | |||
42af2f786f | |||
a65771e271 | |||
7b683d4b57 | |||
a15fd6f966 | |||
19691bba68 | |||
344491ba8e | |||
9f6fa090df | |||
59f4ef03cc | |||
ef92e2a74a | |||
1171984ec2 | |||
ce9746b1f8 | |||
6b99e1c932 | |||
7d418aecd0 | |||
459cdfcf53 | |||
ab8512f96c | |||
6befaf9067 | |||
1b7f74c2b2 |
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
|
about: Report a possible bug in HTTPie
|
||||||
title: ''
|
title: ''
|
||||||
labels: "new, bug"
|
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
|
7
.github/workflows/docs-update-install.yml
vendored
7
.github/workflows/docs-update-install.yml
vendored
@ -3,6 +3,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
|
- .github/workflows/docs-update-install.yml
|
||||||
- docs/installation/*
|
- docs/installation/*
|
||||||
|
|
||||||
# Allow to call the workflow manually
|
# Allow to call the workflow manually
|
||||||
@ -21,6 +22,10 @@ jobs:
|
|||||||
- uses: Automattic/action-commit-to-branch@master
|
- uses: Automattic/action-commit-to-branch@master
|
||||||
with:
|
with:
|
||||||
branch: master
|
branch: master
|
||||||
commit_message: Auto-update installation instructions in the docs
|
commit_message: |
|
||||||
|
Auto-update install docs
|
||||||
|
|
||||||
|
Via .github/workflows/docs-update-install.yml
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
22
.github/workflows/release-snap.yml
vendored
Normal file
22
.github/workflows/release-snap.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
branch:
|
||||||
|
description: "The branch, tag or SHA to release from"
|
||||||
|
required: true
|
||||||
|
default: "master"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
snap:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.branch }}
|
||||||
|
- uses: snapcore/action-build@v1
|
||||||
|
id: build
|
||||||
|
- uses: snapcore/action-publish@v1
|
||||||
|
with:
|
||||||
|
store_login: ${{ secrets.SNAP_STORE_LOGIN }}
|
||||||
|
snap: ${{ steps.build.outputs.snap }}
|
||||||
|
release: edge
|
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -17,6 +17,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- 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
|
- uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
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:
|
paths:
|
||||||
- .github/workflows/test-package-linux-snap.yml
|
- .github/workflows/test-package-linux-snap.yml
|
||||||
- snapcraft.yaml
|
- snapcraft.yaml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
snap:
|
snap:
|
||||||
@ -18,6 +19,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
httpie.http --version
|
httpie.http --version
|
||||||
httpie.https --version
|
httpie.https --version
|
||||||
|
httpie --version
|
||||||
# Auto-aliases cannot be tested when installing a snap outside the store.
|
# Auto-aliases cannot be tested when installing a snap outside the store.
|
||||||
# http --version
|
# http --version
|
||||||
# https --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:
|
paths:
|
||||||
- .github/workflows/test-package-mac-brew.yml
|
- .github/workflows/test-package-mac-brew.yml
|
||||||
- docs/packaging/brew/httpie.rb
|
- docs/packaging/brew/httpie.rb
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
brew:
|
brew:
|
||||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
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]
|
pyopenssl: [0, 1]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
specfile_path: httpie.spec
|
specfile_path: httpie.spec
|
||||||
actions:
|
actions:
|
||||||
# get the current Fedora Rawhide specfile:
|
# 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:
|
jobs:
|
||||||
- job: copr_build
|
- job: copr_build
|
||||||
trigger: pull_request
|
trigger: pull_request
|
||||||
|
36
CHANGELOG.md
36
CHANGELOG.md
@ -3,18 +3,44 @@
|
|||||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [2.6.0.dev0](https://github.com/httpie/httpie/compare/2.5.0...master) (unreleased)
|
## [3.0.0](https://github.com/httpie/httpie/compare/2.6.0...3.0.0) (2022-01-21)
|
||||||
|
|
||||||
|
- Dropped support for Python 3.6. ([#1177](https://github.com/httpie/httpie/issues/1177))
|
||||||
|
- Improved startup time by 40%. ([#1211](https://github.com/httpie/httpie/pull/1211))
|
||||||
|
- Added support for nested JSON syntax. ([#1169](https://github.com/httpie/httpie/issues/1169))
|
||||||
|
- Added `httpie plugins` interface for plugin management. ([#566](https://github.com/httpie/httpie/issues/566))
|
||||||
|
- Added support for Bearer authentication via `--auth-type=bearer` ([#1215](https://github.com/httpie/httpie/issues/1215)).
|
||||||
|
- Added support for quick conversions of pasted URLs into HTTPie calls by adding a space after the protocol name (`$ https ://pie.dev` → `https://pie.dev`). ([#1195](https://github.com/httpie/httpie/issues/1195))
|
||||||
|
- Added support for _sending_ multiple HTTP header lines with the same name. ([#130](https://github.com/httpie/httpie/issues/130))
|
||||||
|
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
|
||||||
|
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
|
||||||
|
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
|
||||||
|
- Added support for displaying the total elapsed time through `--meta`/`-vv` or `--print=m`. ([#243](https://github.com/httpie/httpie/issues/243))
|
||||||
|
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
|
||||||
|
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
|
||||||
|
- Added support for storing prompted passwords in the local sessions. ([#1098](https://github.com/httpie/httpie/issues/1098))
|
||||||
|
- Added warnings about the `--ignore-stdin`, when there is no incoming data from stdin. ([#1255](https://github.com/httpie/httpie/issues/1255))
|
||||||
|
- Fixed crashing due to broken plugins. ([#1204](https://github.com/httpie/httpie/issues/1204))
|
||||||
|
- Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156))
|
||||||
|
- Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242))
|
||||||
|
- Fixed an unexpected crash when `--raw` is used with `--chunked`. ([#1253](https://github.com/httpie/httpie/issues/1253))
|
||||||
|
- Changed the default Windows theme from `fruity` to `auto`. ([#1266](https://github.com/httpie/httpie/issues/1266))
|
||||||
|
|
||||||
|
## [2.6.0](https://github.com/httpie/httpie/compare/2.5.0...2.6.0) (2021-10-14)
|
||||||
|
|
||||||
|
[What’s new in HTTPie 2.6.0 →](https://httpie.io/blog/httpie-2.6.0)
|
||||||
|
|
||||||
- Added support for formatting & coloring of JSON bodies preceded by non-JSON data (e.g., an XXSI prefix). ([#1130](https://github.com/httpie/httpie/issues/1130))
|
- Added support for formatting & coloring of JSON bodies preceded by non-JSON data (e.g., an XXSI prefix). ([#1130](https://github.com/httpie/httpie/issues/1130))
|
||||||
|
- Added charset auto-detection when `Content-Type` doesn’t include it. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
|
||||||
- Added `--response-charset` to allow overriding the response encoding for terminal display purposes. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
- Added `--response-charset` to allow overriding the response encoding for terminal display purposes. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
||||||
- Added `--response-mime` to allow overriding the response mime type for coloring and formatting for the terminal. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
- Added `--response-mime` to allow overriding the response mime type for coloring and formatting for the terminal. ([#1168](https://github.com/httpie/httpie/issues/1168))
|
||||||
- Improved handling of responses with incorrect `Content-Type`. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
|
- Added the ability to silence warnings through using `-q` or `--quiet` twice (e.g. `-qq`) ([#1175](https://github.com/httpie/httpie/issues/1175))
|
||||||
- Installed plugins are now listed in `--debug` output. ([#1165](https://github.com/httpie/httpie/issues/1165))
|
- Added installed plugin list to `--debug` output. ([#1165](https://github.com/httpie/httpie/issues/1165))
|
||||||
- Fixed duplicate keys preservation of JSON data. ([#1163](https://github.com/httpie/httpie/issues/1163))
|
- Fixed duplicate keys preservation in JSON data. ([#1163](https://github.com/httpie/httpie/issues/1163))
|
||||||
|
|
||||||
## [2.5.0](https://github.com/httpie/httpie/compare/2.4.0...2.5.0) (2021-09-06)
|
## [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 2.5.0 →](https://httpie.io/blog/httpie-2.5.0)
|
||||||
|
|
||||||
- Added `--raw` to allow specifying the raw request body without extra processing as
|
- 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))
|
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.
|
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
|
#### Windows
|
||||||
|
|
||||||
If you are on a Windows machine and not able to run `make`,
|
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
|
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:
|
Create a virtual environment and activate it:
|
||||||
|
|
||||||
@ -160,7 +174,7 @@ C:\> venv\Scripts\activate
|
|||||||
Install HTTPie in editable mode with all the dependencies:
|
Install HTTPie in editable mode with all the dependencies:
|
||||||
|
|
||||||
```powershell
|
```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
|
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
|
```powershell
|
||||||
# In 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
|
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
|
```bash
|
||||||
# In CMD:
|
# In CMD:
|
||||||
(httpie) C:\Users\ovezovs\httpie> where http
|
(httpie) C:\Users\<user>\httpie> where http
|
||||||
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
|
C:\Users\<user>\httpie\venv\Scripts\http.exe
|
||||||
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\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
|
2.3.0-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
2
Makefile
2
Makefile
@ -130,7 +130,7 @@ pycodestyle: codestyle
|
|||||||
codestyle:
|
codestyle:
|
||||||
@echo $(H1)Running flake8$(H1END)
|
@echo $(H1)Running flake8$(H1END)
|
||||||
@[ -f $(VENV_BIN)/flake8 ] || $(VENV_PIP) install --upgrade --editable '.[dev]'
|
@[ -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
|
@echo
|
||||||
|
|
||||||
|
|
||||||
|
745
docs/README.md
745
docs/README.md
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"website": {
|
"website": {
|
||||||
"master_and_released_docs_differ_after": "8f8851f1dbd511d3bc0ea0f6da7459045610afce"
|
"master_and_released_docs_differ_after": "d40f06687f8cbbd22bf7dba05bee93aea11a169f"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
docs/contributors/README.md
Normal file
3
docs/contributors/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Here we maintain a database of contributors, from which we generate credits on release blog posts and social medias.
|
||||||
|
|
||||||
|
For the HTTPie blog see: <https://httpie.io/blog>.
|
280
docs/contributors/fetch.py
Normal file
280
docs/contributors/fetch.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
"""
|
||||||
|
Generate the contributors database.
|
||||||
|
|
||||||
|
FIXME: replace `requests` calls with the HTTPie API, when available.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import check_output
|
||||||
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional, Set
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
FullNames = Set[str]
|
||||||
|
GitHubLogins = Set[str]
|
||||||
|
Person = Dict[str, str]
|
||||||
|
People = Dict[str, Person]
|
||||||
|
UserInfo = Dict[str, Any]
|
||||||
|
|
||||||
|
CO_AUTHORS = re.compile(r'Co-authored-by: ([^<]+) <').finditer
|
||||||
|
API_URL = 'https://api.github.com'
|
||||||
|
REPO = OWNER = 'httpie'
|
||||||
|
REPO_URL = f'{API_URL}/repos/{REPO}/{OWNER}'
|
||||||
|
|
||||||
|
HERE = Path(__file__).parent
|
||||||
|
DB_FILE = HERE / 'people.json'
|
||||||
|
|
||||||
|
DEFAULT_PERSON: Person = {'committed': [], 'reported': [], 'github': '', 'twitter': ''}
|
||||||
|
SKIPPED_LABELS = {'invalid'}
|
||||||
|
|
||||||
|
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
|
||||||
|
assert GITHUB_TOKEN, 'GITHUB_TOKEN envar is missing'
|
||||||
|
|
||||||
|
|
||||||
|
class FinishedForNow(Exception):
|
||||||
|
"""Raised when remaining GitHub rate limit is zero."""
|
||||||
|
|
||||||
|
|
||||||
|
def main(previous_release: str, current_release: str) -> int:
|
||||||
|
since = release_date(previous_release)
|
||||||
|
until = release_date(current_release)
|
||||||
|
|
||||||
|
contributors = load_awesome_people()
|
||||||
|
try:
|
||||||
|
committers = find_committers(since, until)
|
||||||
|
reporters = find_reporters(since, until)
|
||||||
|
except Exception as exc:
|
||||||
|
# We want to save what we fetched so far. So pass.
|
||||||
|
print(' !! ', exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
merge_all_the_people(current_release, contributors, committers, reporters)
|
||||||
|
fetch_missing_users_details(contributors)
|
||||||
|
except FinishedForNow:
|
||||||
|
# We want to save what we fetched so far. So pass.
|
||||||
|
print(' !! Committers:', committers)
|
||||||
|
print(' !! Reporters:', reporters)
|
||||||
|
exit_status = 1
|
||||||
|
else:
|
||||||
|
exit_status = 0
|
||||||
|
|
||||||
|
save_awesome_people(contributors)
|
||||||
|
return exit_status
|
||||||
|
|
||||||
|
|
||||||
|
def find_committers(since: str, until: str) -> FullNames:
|
||||||
|
url = f'{REPO_URL}/commits'
|
||||||
|
page = 1
|
||||||
|
per_page = 100
|
||||||
|
params = {
|
||||||
|
'since': since,
|
||||||
|
'until': until,
|
||||||
|
'per_page': per_page,
|
||||||
|
}
|
||||||
|
committers: FullNames = set()
|
||||||
|
|
||||||
|
while 'there are commits':
|
||||||
|
params['page'] = page
|
||||||
|
data = fetch(url, params=params)
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
commit = item['commit']
|
||||||
|
committers.add(commit['author']['name'])
|
||||||
|
debug(' >>> Commit', item['html_url'])
|
||||||
|
for co_author in CO_AUTHORS(commit['message']):
|
||||||
|
name = co_author.group(1)
|
||||||
|
committers.add(name)
|
||||||
|
|
||||||
|
if len(data) < per_page:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return committers
|
||||||
|
|
||||||
|
|
||||||
|
def find_reporters(since: str, until: str) -> GitHubLogins:
|
||||||
|
url = f'{API_URL}/search/issues'
|
||||||
|
page = 1
|
||||||
|
per_page = 100
|
||||||
|
params = {
|
||||||
|
'q': f'repo:{REPO}/{OWNER} is:issue closed:{since}..{until}',
|
||||||
|
'per_page': per_page,
|
||||||
|
}
|
||||||
|
reporters: GitHubLogins = set()
|
||||||
|
|
||||||
|
while 'there are issues':
|
||||||
|
params['page'] = page
|
||||||
|
data = fetch(url, params=params)
|
||||||
|
|
||||||
|
for item in data['items']:
|
||||||
|
# Filter out unwanted labels.
|
||||||
|
if any(label['name'] in SKIPPED_LABELS for label in item['labels']):
|
||||||
|
continue
|
||||||
|
debug(' >>> Issue', item['html_url'])
|
||||||
|
reporters.add(item['user']['login'])
|
||||||
|
|
||||||
|
if len(data['items']) < per_page:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
return reporters
|
||||||
|
|
||||||
|
|
||||||
|
def merge_all_the_people(release: str, contributors: People, committers: FullNames, reporters: GitHubLogins) -> None:
|
||||||
|
"""
|
||||||
|
>>> contributors = {'Alice': new_person(github='alice', twitter='alice')}
|
||||||
|
>>> merge_all_the_people('2.6.0', contributors, {}, {})
|
||||||
|
>>> contributors
|
||||||
|
{'Alice': {'committed': [], 'reported': [], 'github': 'alice', 'twitter': 'alice'}}
|
||||||
|
|
||||||
|
>>> contributors = {'Bob': new_person(github='bob', twitter='bob')}
|
||||||
|
>>> merge_all_the_people('2.6.0', contributors, {'Bob'}, {'bob'})
|
||||||
|
>>> contributors
|
||||||
|
{'Bob': {'committed': ['2.6.0'], 'reported': ['2.6.0'], 'github': 'bob', 'twitter': 'bob'}}
|
||||||
|
|
||||||
|
>>> contributors = {'Charlotte': new_person(github='charlotte', twitter='charlotte', committed=['2.5.0'], reported=['2.5.0'])}
|
||||||
|
>>> merge_all_the_people('2.6.0', contributors, {'Charlotte'}, {'charlotte'})
|
||||||
|
>>> contributors
|
||||||
|
{'Charlotte': {'committed': ['2.5.0', '2.6.0'], 'reported': ['2.5.0', '2.6.0'], 'github': 'charlotte', 'twitter': 'charlotte'}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Update known contributors.
|
||||||
|
for name, details in contributors.items():
|
||||||
|
if name in committers:
|
||||||
|
if release not in details['committed']:
|
||||||
|
details['committed'].append(release)
|
||||||
|
committers.remove(name)
|
||||||
|
if details['github'] in reporters:
|
||||||
|
if release not in details['reported']:
|
||||||
|
details['reported'].append(release)
|
||||||
|
reporters.remove(details['github'])
|
||||||
|
|
||||||
|
# Add new committers.
|
||||||
|
for name in committers:
|
||||||
|
user_info = user(fullname=name)
|
||||||
|
contributors[name] = new_person(
|
||||||
|
github=user_info['login'],
|
||||||
|
twitter=user_info['twitter_username'],
|
||||||
|
committed=[release],
|
||||||
|
)
|
||||||
|
if user_info['login'] in reporters:
|
||||||
|
contributors[name]['reported'].append(release)
|
||||||
|
reporters.remove(user_info['login'])
|
||||||
|
|
||||||
|
# Add new reporters.
|
||||||
|
for github_username in reporters:
|
||||||
|
user_info = user(github_username=github_username)
|
||||||
|
contributors[user_info['name'] or user_info['login']] = new_person(
|
||||||
|
github=github_username,
|
||||||
|
twitter=user_info['twitter_username'],
|
||||||
|
reported=[release],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def release_date(release: str) -> str:
|
||||||
|
date = check_output(['git', 'log', '-1', '--format=%ai', release], text=True).strip()
|
||||||
|
return datetime.strptime(date, '%Y-%m-%d %H:%M:%S %z').isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def load_awesome_people() -> People:
|
||||||
|
try:
|
||||||
|
with DB_FILE.open(encoding='utf-8') as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url: str, params: Optional[Dict[str, str]] = None) -> UserInfo:
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/vnd.github.v3+json',
|
||||||
|
'Authentication': f'token {GITHUB_TOKEN}'
|
||||||
|
}
|
||||||
|
for retry in range(1, 6):
|
||||||
|
debug(f'[{retry}/5]', f'{url = }', f'{params = }')
|
||||||
|
with requests.get(url, params=params, headers=headers) as req:
|
||||||
|
try:
|
||||||
|
req.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as exc:
|
||||||
|
if exc.response.status_code == 403:
|
||||||
|
# 403 Client Error: rate limit exceeded for url: ...
|
||||||
|
now = int(datetime.utcnow().timestamp())
|
||||||
|
xrate_limit_reset = int(exc.response.headers['X-RateLimit-Reset'])
|
||||||
|
wait = xrate_limit_reset - now
|
||||||
|
if wait > 20:
|
||||||
|
raise FinishedForNow()
|
||||||
|
debug(' !', 'Waiting', wait, 'seconds before another try ...')
|
||||||
|
sleep(wait)
|
||||||
|
continue
|
||||||
|
return req.json()
|
||||||
|
assert ValueError('Rate limit exceeded')
|
||||||
|
|
||||||
|
|
||||||
|
def new_person(**kwargs: str) -> Person:
|
||||||
|
data = deepcopy(DEFAULT_PERSON)
|
||||||
|
data.update(**kwargs)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def user(fullname: Optional[str] = '', github_username: Optional[str] = '') -> UserInfo:
|
||||||
|
if github_username:
|
||||||
|
url = f'{API_URL}/users/{github_username}'
|
||||||
|
return fetch(url)
|
||||||
|
|
||||||
|
url = f'{API_URL}/search/users'
|
||||||
|
for query in (f'fullname:{fullname}', f'user:{fullname}'):
|
||||||
|
params = {
|
||||||
|
'q': f'repo:{REPO}/{OWNER} {query}',
|
||||||
|
'per_page': 1,
|
||||||
|
}
|
||||||
|
user_info = fetch(url, params=params)
|
||||||
|
if user_info['items']:
|
||||||
|
user_url = user_info['items'][0]['url']
|
||||||
|
return fetch(user_url)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_missing_users_details(people: People) -> None:
|
||||||
|
for name, details in people.items():
|
||||||
|
if details['github'] and details['twitter']:
|
||||||
|
continue
|
||||||
|
user_info = user(github_username=details['github'], fullname=name)
|
||||||
|
if not details['github']:
|
||||||
|
details['github'] = user_info['login']
|
||||||
|
if not details['twitter']:
|
||||||
|
details['twitter'] = user_info['twitter_username']
|
||||||
|
|
||||||
|
|
||||||
|
def save_awesome_people(people: People) -> None:
|
||||||
|
with DB_FILE.open(mode='w', encoding='utf-8') as fh:
|
||||||
|
json.dump(people, fh, indent=4, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def debug(*args: Any) -> None:
|
||||||
|
if os.getenv('DEBUG') == '1':
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ret = 1
|
||||||
|
try:
|
||||||
|
ret = main(*sys.argv[1:])
|
||||||
|
except TypeError:
|
||||||
|
ret = 2
|
||||||
|
print(f'''
|
||||||
|
Fetch contributors to a release.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python {sys.argv[0]} {sys.argv[0]} <RELEASE N-1> <RELEASE N>
|
||||||
|
Example:
|
||||||
|
python {sys.argv[0]} 2.4.0 2.5.0
|
||||||
|
|
||||||
|
Define the DEBUG=1 environment variable to enable verbose output.
|
||||||
|
''')
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
ret = 255
|
||||||
|
sys.exit(ret)
|
45
docs/contributors/generate.py
Normal file
45
docs/contributors/generate.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Generate snippets to copy-paste.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from fetch import HERE, load_awesome_people
|
||||||
|
|
||||||
|
TPL_FILE = HERE / 'snippet.jinja2'
|
||||||
|
HTTPIE_TEAM = {
|
||||||
|
'claudiatd',
|
||||||
|
'jakubroztocil',
|
||||||
|
'jkbr',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_snippets(release: str) -> str:
|
||||||
|
people = load_awesome_people()
|
||||||
|
contributors = {
|
||||||
|
name: details
|
||||||
|
for name, details in people.items()
|
||||||
|
if details['github'] not in HTTPIE_TEAM
|
||||||
|
and (release in details['committed'] or release in details['reported'])
|
||||||
|
}
|
||||||
|
|
||||||
|
template = Template(source=TPL_FILE.read_text(encoding='utf-8'))
|
||||||
|
output = template.render(contributors=contributors, release=release)
|
||||||
|
print(output)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ret = 1
|
||||||
|
try:
|
||||||
|
ret = generate_snippets(sys.argv[1])
|
||||||
|
except (IndexError, TypeError):
|
||||||
|
ret = 2
|
||||||
|
print(f'''
|
||||||
|
Generate snippets for contributors to a release.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python {sys.argv[0]} {sys.argv[0]} <RELEASE>
|
||||||
|
''')
|
||||||
|
sys.exit(ret)
|
329
docs/contributors/people.json
Normal file
329
docs/contributors/people.json
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
{
|
||||||
|
"Almad": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "Almad",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": "almadcz"
|
||||||
|
},
|
||||||
|
"Annette Wilson": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "annettejanewilson",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Anton Emelyanov": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "king-menin",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"D8ger": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "caofanCPU",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Dave": {
|
||||||
|
"committed": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "davecheney",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": "davecheney"
|
||||||
|
},
|
||||||
|
"Dawid Ferenczy Rogo\u017ean": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "ferenczy",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": "DawidFerenczy"
|
||||||
|
},
|
||||||
|
"Elena Lape": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "elenalape",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": "elena_lape"
|
||||||
|
},
|
||||||
|
"Fabio Peruzzo": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "peruzzof",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"F\u00fash\u0113ng": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "lienide",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Giampaolo Rodola": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "giampaolo",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Hugh Williams": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "hughpv",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Ilya Sukhanov": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "IlyaSukhanov",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Jakub Roztocil": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0",
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "jakubroztocil",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0",
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": "jakubroztocil"
|
||||||
|
},
|
||||||
|
"Jan Verbeek": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "blyxxyz",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Jannik Vieten": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "exploide",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Marcel St\u00f6r": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "marcelstoer",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": "frightanic"
|
||||||
|
},
|
||||||
|
"Mariano Ruiz": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "mrsarm",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": "mrsarm82"
|
||||||
|
},
|
||||||
|
"Micka\u00ebl Schoentgen": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0",
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "BoboTiG",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0",
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": "__tiger222__"
|
||||||
|
},
|
||||||
|
"Miro Hron\u010dok": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0",
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "hroncok",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": "hroncok"
|
||||||
|
},
|
||||||
|
"Mohamed Daahir": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "ducaale",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Omer Akram": {
|
||||||
|
"committed": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "om26er",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": "om26er"
|
||||||
|
},
|
||||||
|
"Pavel Alexeev aka Pahan-Hubbitus": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "Hubbitus",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Samuel Marks": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "SamuelMarks",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Sullivan SENECHAL": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "soullivaneuh",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Thomas Klinger": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "mosesontheweb",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Vincent van \u2019t Zand": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "vovtz",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"Yannic Schneider": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "cynay",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"a1346054": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "a1346054",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"bl-ue": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "FiReBlUe45",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"claudiatd": {
|
||||||
|
"committed": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "claudiatd",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"dkreeft": {
|
||||||
|
"committed": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "dkreeft",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"henryhu712": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "henryhu712",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"jakubroztocil": {
|
||||||
|
"committed": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"github": "jkbr",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"jungle-boogie": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "jungle-boogie",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"nixbytes": {
|
||||||
|
"committed": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"github": "nixbytes",
|
||||||
|
"reported": [],
|
||||||
|
"twitter": "linuxbyte3"
|
||||||
|
},
|
||||||
|
"qiulang": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "qiulang",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"zwx00": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "zwx00",
|
||||||
|
"reported": [
|
||||||
|
"2.5.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"\u5d14\u5c0f\u4e8c": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "rogerdehe",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
},
|
||||||
|
"\u9ec4\u6d77": {
|
||||||
|
"committed": [],
|
||||||
|
"github": "hh-in-zhuzhou",
|
||||||
|
"reported": [
|
||||||
|
"2.6.0"
|
||||||
|
],
|
||||||
|
"twitter": null
|
||||||
|
}
|
||||||
|
}
|
13
docs/contributors/snippet.jinja2
Normal file
13
docs/contributors/snippet.jinja2
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!-- Blog post -->
|
||||||
|
|
||||||
|
## Community contributions
|
||||||
|
|
||||||
|
We’d like to thank these amazing people for their contributions to this release: {% for name, details in contributors.items() -%}
|
||||||
|
[{{ name }}](https://github.com/{{ details.github }}){{ '' if loop.last else ', ' }}
|
||||||
|
{%- endfor %}.
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
|
||||||
|
We’d like to thank these amazing people for their contributions to HTTPie {{ release }}: {% for name, details in contributors.items() if details.twitter -%}
|
||||||
|
@{{ details.twitter }}{{ '' if loop.last else ', ' }}
|
||||||
|
{%- endfor %} 🥧
|
@ -19,16 +19,16 @@ Do not edit here, but in docs/installation/.
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if tool.links.setup %}
|
{% if tool.links.setup %}
|
||||||
To install [{{ tool.name }}]({{ tool.links.homepage }}) follow [installation instructions]({{ tool.links.setup }}).
|
To install [{{ tool.name }}]({{ tool.links.homepage }}), see [its installation]({{ tool.links.setup }}).
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install
|
# Install httpie
|
||||||
$ {{ tool.commands.install|join('\n$ ') }}
|
$ {{ tool.commands.install|join('\n$ ') }}
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Upgrade
|
# Upgrade httpie
|
||||||
$ {{ tool.commands.upgrade|join('\n$ ') }}
|
$ {{ tool.commands.upgrade|join('\n$ ') }}
|
||||||
```
|
```
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -14,8 +14,6 @@ docs-structure:
|
|||||||
macOS:
|
macOS:
|
||||||
- brew-mac
|
- brew-mac
|
||||||
- port
|
- port
|
||||||
- snap-mac
|
|
||||||
- spack-mac
|
|
||||||
Windows:
|
Windows:
|
||||||
- chocolatey
|
- chocolatey
|
||||||
Linux:
|
Linux:
|
||||||
@ -24,29 +22,11 @@ docs-structure:
|
|||||||
- apt
|
- apt
|
||||||
- dnf
|
- dnf
|
||||||
- yum
|
- yum
|
||||||
- apk
|
|
||||||
- emerge
|
|
||||||
- pacman
|
- pacman
|
||||||
- xbps-install
|
|
||||||
- spack-linux
|
|
||||||
FreeBSD:
|
FreeBSD:
|
||||||
- pkg
|
- pkg
|
||||||
|
|
||||||
tools:
|
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:
|
apt:
|
||||||
title: Debian and Ubuntu
|
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.
|
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.
|
||||||
@ -113,26 +93,10 @@ tools:
|
|||||||
package: https://src.fedoraproject.org/rpms/httpie
|
package: https://src.fedoraproject.org/rpms/httpie
|
||||||
commands:
|
commands:
|
||||||
install:
|
install:
|
||||||
- dnf update
|
|
||||||
- dnf install httpie
|
- dnf install httpie
|
||||||
upgrade:
|
upgrade:
|
||||||
- dnf update
|
|
||||||
- dnf upgrade httpie
|
- 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:
|
pacman:
|
||||||
title: Arch Linux
|
title: Arch Linux
|
||||||
name: pacman
|
name: pacman
|
||||||
@ -174,9 +138,9 @@ tools:
|
|||||||
- port upgrade httpie
|
- port upgrade httpie
|
||||||
|
|
||||||
pypi:
|
pypi:
|
||||||
title: PyPi
|
title: PyPI
|
||||||
name: pip
|
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:
|
links:
|
||||||
homepage: https://pypi.org/
|
homepage: https://pypi.org/
|
||||||
# setup: https://pip.pypa.io/en/stable/installation/
|
# setup: https://pip.pypa.io/en/stable/installation/
|
||||||
@ -202,56 +166,6 @@ tools:
|
|||||||
upgrade:
|
upgrade:
|
||||||
- snap refresh httpie
|
- snap refresh httpie
|
||||||
|
|
||||||
snap-mac:
|
|
||||||
title: Snapcraft (macOS)
|
|
||||||
name: Snapcraft
|
|
||||||
links:
|
|
||||||
homepage: https://snapcraft.io/
|
|
||||||
setup: https://snapcraft.io/docs/installing-snapd
|
|
||||||
package: https://snapcraft.io/httpie
|
|
||||||
commands:
|
|
||||||
install:
|
|
||||||
- snap install httpie
|
|
||||||
upgrade:
|
|
||||||
- snap refresh httpie
|
|
||||||
|
|
||||||
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:
|
yum:
|
||||||
title: CentOS and RHEL
|
title: CentOS and RHEL
|
||||||
name: Yum
|
name: Yum
|
||||||
@ -261,9 +175,7 @@ tools:
|
|||||||
package: https://src.fedoraproject.org/rpms/httpie
|
package: https://src.fedoraproject.org/rpms/httpie
|
||||||
commands:
|
commands:
|
||||||
install:
|
install:
|
||||||
- yum update
|
|
||||||
- yum install epel-release
|
- yum install epel-release
|
||||||
- yum install httpie
|
- yum install httpie
|
||||||
upgrade:
|
upgrade:
|
||||||
- yum update
|
|
||||||
- yum upgrade httpie
|
- yum upgrade httpie
|
||||||
|
@ -17,6 +17,9 @@ exclude_rule 'MD013'
|
|||||||
# MD014 Dollar signs used before commands without showing output
|
# MD014 Dollar signs used before commands without showing output
|
||||||
exclude_rule 'MD014'
|
exclude_rule 'MD014'
|
||||||
|
|
||||||
|
# MD028 Blank line inside blockquote
|
||||||
|
exclude_rule 'MD028'
|
||||||
|
|
||||||
# Tell the linter to use ordered lists:
|
# Tell the linter to use ordered lists:
|
||||||
# 1. Foo
|
# 1. Foo
|
||||||
# 2. Bar
|
# 2. Bar
|
||||||
|
@ -12,18 +12,20 @@ You are looking at the HTTPie packaging documentation, where you will find valua
|
|||||||
|
|
||||||
The overall release process starts simple:
|
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.
|
2. Then, handle company-related tasks.
|
||||||
3. Finally, follow OS-specific steps, described in documents below, to send patches downstream.
|
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/).
|
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).
|
That is done quite easily by manually triggering the [release workflow](https://github.com/httpie/httpie/actions/workflows/release.yml).
|
||||||
|
|
||||||
## Then, company-specific tasks
|
## Then, company-specific tasks
|
||||||
|
|
||||||
- Update the HTTPie version bundled into termible ([example](https://github.com/httpie/termible/pull/1)).
|
- 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
|
## Finally, spread dowstream
|
||||||
|
|
||||||
@ -32,18 +34,13 @@ A more complete state of deployment can be found on [repology](https://repology.
|
|||||||
|
|
||||||
| OS | Maintainer |
|
| OS | Maintainer |
|
||||||
| -------------------------------------------: | -------------- |
|
| -------------------------------------------: | -------------- |
|
||||||
| [Alpine](linux-alpine/) | **HTTPie** |
|
|
||||||
| [Arch Linux, and derived](linux-arch/) | trusted person |
|
| [Arch Linux, and derived](linux-arch/) | trusted person |
|
||||||
| :construction: [AOSC OS](linux-aosc/) | **HTTPie** |
|
|
||||||
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
|
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
|
||||||
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
|
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
|
||||||
| [Fedora](linux-fedora/) | trusted person |
|
| [Fedora](linux-fedora/) | trusted person |
|
||||||
| [Gentoo](linux-gentoo/) | **HTTPie** |
|
|
||||||
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
||||||
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
|
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
|
||||||
| [Snapcraft](snapcraft/) | **HTTPie** |
|
| [Snapcraft](snapcraft/) | **HTTPie** |
|
||||||
| [Spack](spack/) | **HTTPie** |
|
|
||||||
| [Void Linux](linux-void/) | **HTTPie** |
|
|
||||||
| [Windows — Chocolatey](windows-chocolatey/) | **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/).
|
: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.
|
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
|
## Hacking
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ PACKAGES = [
|
|||||||
'requests',
|
'requests',
|
||||||
'requests-toolbelt',
|
'requests-toolbelt',
|
||||||
'urllib3',
|
'urllib3',
|
||||||
|
'multidict',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,29 +3,31 @@ class Httpie < Formula
|
|||||||
|
|
||||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||||
homepage "https://httpie.io/"
|
homepage "https://httpie.io/"
|
||||||
url "https://files.pythonhosted.org/packages/90/64/7ea8066309970f787653bdc8c5328272a5c4d06cbce3a07a6a5c3199c3d7/httpie-2.5.0.tar.gz"
|
url "https://files.pythonhosted.org/packages/53/96/cbcfec73c186f076e4443faf3d91cbbc868f18f6323703afd348b1aba46d/httpie-2.6.0.tar.gz"
|
||||||
sha256 "fe6a8bc50fb0635a84ebe1296a732e39357c3e1354541bf51a7057b4877e47f9"
|
sha256 "ef929317b239bbf0a5bb7159b4c5d2edbfc55f8a0bcf9cd24ce597daec2afca5"
|
||||||
license "BSD-3-Clause"
|
license "BSD-3-Clause"
|
||||||
head "https://github.com/httpie/httpie.git"
|
head "https://github.com/httpie/httpie.git", branch: "master"
|
||||||
|
|
||||||
bottle do
|
bottle do
|
||||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "01115f69aff0399b3f73af09899a42a14343638a4624a35749059cc732c49cdc"
|
sha256 cellar: :any_skip_relocation, arm64_monterey: "83aab05ffbcd4c3baa6de6158d57ebdaa67c148bef8c872527d90bdaebff0504"
|
||||||
sha256 cellar: :any_skip_relocation, big_sur: "53f07157f00edf8193b7d4f74f247f53e1796fbc3e675cd2fbaa4b9dc2bad62c"
|
sha256 cellar: :any_skip_relocation, arm64_big_sur: "3c3a5c2458d0658e14b663495e115297c573aa3466d292f12d02c3ec13a24bdf"
|
||||||
sha256 cellar: :any_skip_relocation, catalina: "7cf216fdee98208856d654060fdcad3968623d7ed27fcdeba27d3120354c9a9f"
|
sha256 cellar: :any_skip_relocation, monterey: "f860e7d3b77dca4928a2c5e10c4cbd50d792330dfb99f7d736ca0da9fb9dd0d0"
|
||||||
sha256 cellar: :any_skip_relocation, mojave: "28adb5aed8c1c2b39c51789f242ff0dffde39073e161deb379c79184d787d063"
|
sha256 cellar: :any_skip_relocation, big_sur: "377b0643aa1f6d310ba4cfc70d66a94cc458213db8d134940d3b10a32defacf1"
|
||||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "91cb8c332c643bd8b1d0a8f3ec0acd4770b407991f6de1fd320d675f2b2e95ec"
|
sha256 cellar: :any_skip_relocation, catalina: "6d306c30f6f1d7a551d88415efe12b7c3f25d0602f3579dc632771a463f78fa5"
|
||||||
|
sha256 cellar: :any_skip_relocation, mojave: "f66b8cdff9cb7b44a84197c3e3d81d810f7ff8f2188998b977ccadfc7e2ec893"
|
||||||
|
sha256 cellar: :any_skip_relocation, x86_64_linux: "53f036b0114814c28982e8c022dcf494e7024de088641d7076fd73d12a45a0e9"
|
||||||
end
|
end
|
||||||
|
|
||||||
depends_on "python@3.9"
|
depends_on "python@3.10"
|
||||||
|
|
||||||
resource "certifi" do
|
resource "certifi" do
|
||||||
url "https://files.pythonhosted.org/packages/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz"
|
url "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz"
|
||||||
sha256 "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"
|
sha256 "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"
|
||||||
end
|
end
|
||||||
|
|
||||||
resource "charset-normalizer" do
|
resource "charset-normalizer" do
|
||||||
url "https://files.pythonhosted.org/packages/e7/4e/2af0238001648ded297fb54ceb425ca26faa15b341b4fac5371d3938666e/charset-normalizer-2.0.4.tar.gz"
|
url "https://files.pythonhosted.org/packages/48/44/76b179e0d1afe6e6a91fd5661c284f60238987f3b42b676d141d01cd5b97/charset-normalizer-2.0.10.tar.gz"
|
||||||
sha256 "f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
sha256 "876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"
|
||||||
end
|
end
|
||||||
|
|
||||||
resource "defusedxml" do
|
resource "defusedxml" do
|
||||||
@ -39,8 +41,8 @@ class Httpie < Formula
|
|||||||
end
|
end
|
||||||
|
|
||||||
resource "Pygments" do
|
resource "Pygments" do
|
||||||
url "https://files.pythonhosted.org/packages/b7/b3/5cba26637fe43500d4568d0ee7b7362de1fb29c0e158d50b4b69e9a40422/Pygments-2.10.0.tar.gz"
|
url "https://files.pythonhosted.org/packages/94/9c/cb656d06950268155f46d4f6ce25d7ffc51a0da47eadf1b164bbf23b718b/Pygments-2.11.2.tar.gz"
|
||||||
sha256 "f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
|
sha256 "4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
|
||||||
end
|
end
|
||||||
|
|
||||||
resource "PySocks" do
|
resource "PySocks" do
|
||||||
@ -49,8 +51,8 @@ class Httpie < Formula
|
|||||||
end
|
end
|
||||||
|
|
||||||
resource "requests" do
|
resource "requests" do
|
||||||
url "https://files.pythonhosted.org/packages/e7/01/3569e0b535fb2e4a6c384bdbed00c55b9d78b5084e0fb7f4d0bf523d7670/requests-2.26.0.tar.gz"
|
url "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz"
|
||||||
sha256 "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
sha256 "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"
|
||||||
end
|
end
|
||||||
|
|
||||||
resource "requests-toolbelt" do
|
resource "requests-toolbelt" do
|
||||||
@ -59,8 +61,13 @@ class Httpie < Formula
|
|||||||
end
|
end
|
||||||
|
|
||||||
resource "urllib3" do
|
resource "urllib3" do
|
||||||
url "https://files.pythonhosted.org/packages/4f/5a/597ef5911cb8919efe4d86206aa8b2658616d676a7088f0825ca08bd7cb8/urllib3-1.26.6.tar.gz"
|
url "https://files.pythonhosted.org/packages/b0/b1/7bbf5181f8e3258efae31702f5eab87d8a74a72a0aa78bc8c08c1466e243/urllib3-1.26.8.tar.gz"
|
||||||
sha256 "f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
sha256 "0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||||
|
end
|
||||||
|
|
||||||
|
resource "multidict" do
|
||||||
|
url "https://files.pythonhosted.org/packages/8e/7c/e12a69795b7b7d5071614af2c691c97fbf16a2a513c66ec52dd7d0a115bb/multidict-5.2.0.tar.gz"
|
||||||
|
sha256 "0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"
|
||||||
end
|
end
|
||||||
|
|
||||||
def install
|
def install
|
||||||
@ -68,6 +75,11 @@ class Httpie < Formula
|
|||||||
end
|
end
|
||||||
|
|
||||||
test do
|
test do
|
||||||
|
# shell_output() already checks the status code
|
||||||
|
shell_output("#{bin}/httpie -v")
|
||||||
|
shell_output("#{bin}/https -v")
|
||||||
|
shell_output("#{bin}/http -v")
|
||||||
|
|
||||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||||
end
|
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>
|
# Contributor: Thomas Weißschuh <thomas_weissschuh lavabit com>
|
||||||
|
|
||||||
pkgname=httpie
|
pkgname=httpie
|
||||||
pkgver=2.5.0
|
pkgver=2.6.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="human-friendly CLI HTTP client for the API era"
|
pkgdesc="human-friendly CLI HTTP client for the API era"
|
||||||
url="https://github.com/httpie/httpie"
|
url="https://github.com/httpie/httpie"
|
||||||
@ -12,7 +12,8 @@ depends=('python-defusedxml'
|
|||||||
'python-pygments'
|
'python-pygments'
|
||||||
'python-pysocks'
|
'python-pysocks'
|
||||||
'python-requests'
|
'python-requests'
|
||||||
'python-requests-toolbelt')
|
'python-requests-toolbelt'
|
||||||
|
'python-charset-normalizer')
|
||||||
makedepends=('python-setuptools')
|
makedepends=('python-setuptools')
|
||||||
checkdepends=('python-pytest'
|
checkdepends=('python-pytest'
|
||||||
'python-pytest-httpbin'
|
'python-pytest-httpbin'
|
||||||
@ -22,7 +23,7 @@ replaces=(python-httpie python2-httpie)
|
|||||||
license=('BSD')
|
license=('BSD')
|
||||||
arch=('any')
|
arch=('any')
|
||||||
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
|
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
|
||||||
sha256sums=('66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea')
|
sha256sums=('3bcd9a8cb2b11299da12d3af36c095c6d4b665e41c395898a07f1ae4d99fc14a')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd $pkgname-$pkgver
|
cd $pkgname-$pkgver
|
||||||
@ -42,5 +43,5 @@ package() {
|
|||||||
|
|
||||||
check() {
|
check() {
|
||||||
cd $pkgname-$pkgver
|
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>`
|
- To: `Debian Bug Tracking System <submit@bugs.debian.org>`
|
||||||
- Subject: `httpie: Version XXX available`
|
- 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
|
```email
|
||||||
Package: httpie
|
Package: httpie
|
||||||
Severity: wishlist
|
Severity: normal
|
||||||
|
|
||||||
<MESSAGE>
|
<MESSAGE>
|
||||||
```
|
```
|
||||||
|
@ -42,7 +42,7 @@ Q: Are new versions backported automatically?
|
|||||||
A: No. The process is:
|
A: No. The process is:
|
||||||
|
|
||||||
1. A new HTTPie release is created on Github.
|
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).
|
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.
|
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 github 1.0
|
||||||
PortGroup python 1.0
|
PortGroup python 1.0
|
||||||
|
|
||||||
github.setup httpie httpie 2.5.0
|
github.setup httpie httpie 2.6.0
|
||||||
|
|
||||||
maintainers {g5pw @g5pw} openmaintainer
|
maintainers {g5pw @g5pw} openmaintainer
|
||||||
categories net
|
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 \
|
long_description HTTPie (pronounced aych-tee-tee-pie) is a command line HTTP \
|
||||||
client. Its goal is to make CLI interaction with web \
|
client. Its goal is to make CLI interaction with web \
|
||||||
services as human-friendly as possible. It provides a simple \
|
services as human-friendly as possible. It provides a simple \
|
||||||
@ -20,17 +20,17 @@ platforms darwin
|
|||||||
license BSD
|
license BSD
|
||||||
homepage https://httpie.io/
|
homepage https://httpie.io/
|
||||||
|
|
||||||
variant python36 conflicts python37 python38 python39 description "Use Python 3.6" {}
|
variant python37 conflicts python36 python38 python39 python310 description "Use Python 3.7" {}
|
||||||
variant python37 conflicts python36 python38 python39 description "Use Python 3.7" {}
|
variant python38 conflicts python36 python37 python39 python310 description "Use Python 3.8" {}
|
||||||
variant python38 conflicts python36 python37 python39 description "Use Python 3.8" {}
|
variant python39 conflicts python36 python37 python38 python310 description "Use Python 3.9" {}
|
||||||
variant python39 conflicts python36 python37 python38 description "Use Python 3.9" {}
|
variant python310 conflicts python36 python37 python38 python39 description "Use Python 3.10" {}
|
||||||
|
|
||||||
if {[variant_isset python36]} {
|
if {[variant_isset python37]} {
|
||||||
python.default_version 36
|
|
||||||
} elseif {[variant_isset python37]} {
|
|
||||||
python.default_version 37
|
python.default_version 37
|
||||||
} elseif {[variant_isset python39]} {
|
} elseif {[variant_isset python39]} {
|
||||||
python.default_version 39
|
python.default_version 39
|
||||||
|
} elseif {[variant_isset python310]} {
|
||||||
|
python.default_version 310
|
||||||
} else {
|
} else {
|
||||||
default_variants +python38
|
default_variants +python38
|
||||||
python.default_version 38
|
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}-requests-toolbelt \
|
||||||
port:py${python.version}-pygments \
|
port:py${python.version}-pygments \
|
||||||
port:py${python.version}-socks \
|
port:py${python.version}-socks \
|
||||||
|
port:py${python.version}-charset-normalizer \
|
||||||
port:py${python.version}-defusedxml
|
port:py${python.version}-defusedxml
|
||||||
|
|
||||||
checksums rmd160 88d227d52199c232c0ddf704a219d1781b1e77ee \
|
checksums rmd160 07b1d1592da1c505ed3ee4ef3b6056215e16e9ff \
|
||||||
sha256 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc \
|
sha256 63cf104bf3552305c68a74f16494a90172b15296610a875e17918e5e36373c0b \
|
||||||
size 1105185
|
size 1133491
|
||||||
|
|
||||||
python.link_binaries_suffix
|
python.link_binaries_suffix
|
||||||
|
@ -13,7 +13,7 @@ We will discuss setting up the environment, installing development tools, instal
|
|||||||
|
|
||||||
## Overall process
|
## 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):
|
- 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">
|
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||||
<metadata>
|
<metadata>
|
||||||
<id>httpie</id>
|
<id>httpie</id>
|
||||||
<version>2.5.0</version>
|
<version>2.6.0</version>
|
||||||
<summary>Modern, user-friendly command-line HTTP client for the API era.</summary>
|
<summary>Modern, user-friendly command-line HTTP client for the API era</summary>
|
||||||
<description>
|
<description>
|
||||||
HTTPie *aitch-tee-tee-pie* is a user-friendly command-line HTTP client for the API era.
|
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.
|
It comes with JSON support, syntax highlighting, persistent sessions, wget-like downloads, plugins, and more.
|
||||||
@ -28,20 +28,20 @@ Main features:
|
|||||||
</description>
|
</description>
|
||||||
<title>HTTPie</title>
|
<title>HTTPie</title>
|
||||||
<authors>HTTPie</authors>
|
<authors>HTTPie</authors>
|
||||||
<owners>Tiger-222</owners>
|
<owners>jakubroztocil</owners>
|
||||||
<copyright>2012-2021 Jakub Roztocil</copyright>
|
<copyright>2012-2021 Jakub Roztocil</copyright>
|
||||||
<licenseUrl>https://raw.githubusercontent.com/httpie/httpie/master/LICENSE</licenseUrl>
|
<licenseUrl>https://raw.githubusercontent.com/httpie/httpie/master/LICENSE</licenseUrl>
|
||||||
<iconUrl>https://pie-assets.s3.eu-central-1.amazonaws.com/LogoIcons/GB.png</iconUrl>
|
<iconUrl>https://pie-assets.s3.eu-central-1.amazonaws.com/LogoIcons/GB.png</iconUrl>
|
||||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
<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>
|
<tags>httpie http https rest api client curl python ssl cli foss oss url</tags>
|
||||||
<projectUrl>https://httpie.io</projectUrl>
|
<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>
|
<projectSourceUrl>https://github.com/httpie/httpie</projectSourceUrl>
|
||||||
<docsUrl>https://httpie.io/docs</docsUrl>
|
<docsUrl>https://httpie.io/docs</docsUrl>
|
||||||
<bugTrackerUrl>https://github.com/httpie/httpie/issues</bugTrackerUrl>
|
<bugTrackerUrl>https://github.com/httpie/httpie/issues</bugTrackerUrl>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency id="python3" version="3.6" />
|
<dependency id="python3" version="3.7" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</metadata>
|
</metadata>
|
||||||
<files>
|
<files>
|
||||||
|
@ -1,6 +1,2 @@
|
|||||||
$ErrorActionPreference = 'Stop';
|
$ErrorActionPreference = 'Stop';
|
||||||
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
|
py -m pip install $env:ChocolateyPackageName==$env:ChocolateyPackageVersion --disable-pip-version-check
|
||||||
$nuspecPath = "$(Join-Path (Split-Path -parent $toolsDir) ($env:ChocolateyPackageName + ".nuspec"))"
|
|
||||||
[XML]$nuspec = Get-Content $nuspecPath
|
|
||||||
$pipVersion = $nuspec.package.metadata.version
|
|
||||||
py -m pip install "$($env:ChocolateyPackageName)==$($pipVersion)" --disable-pip-version-check
|
|
||||||
|
@ -7,7 +7,7 @@ _http_complete() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
complete -o default -F _http_complete http
|
complete -o default -F _http_complete http httpie.http httpie.https https
|
||||||
|
|
||||||
_http_complete_options() {
|
_http_complete_options() {
|
||||||
local cur_word=$1
|
local cur_word=$1
|
||||||
|
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.dev0'
|
__version__ = '3.0.0'
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__licence__ = 'BSD'
|
__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,
|
parse_format_options,
|
||||||
)
|
)
|
||||||
from .constants import (
|
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,
|
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
|
||||||
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
||||||
SEPARATOR_CREDENTIALS,
|
SEPARATOR_CREDENTIALS,
|
||||||
@ -50,7 +50,64 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
|||||||
|
|
||||||
# TODO: refactor and design type-annotated data structures
|
# TODO: refactor and design type-annotated data structures
|
||||||
# for raw args + parsed args and keep things immutable.
|
# 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`.
|
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||||
|
|
||||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs['add_help'] = False
|
kwargs.setdefault('add_help', False)
|
||||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.env = None
|
|
||||||
self.args = None
|
|
||||||
self.has_stdin_data = False
|
|
||||||
self.has_input_data = False
|
|
||||||
|
|
||||||
# noinspection PyMethodOverriding
|
# noinspection PyMethodOverriding
|
||||||
def parse_args(
|
def parse_args(
|
||||||
@ -120,6 +173,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _process_url(self):
|
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 not URL_SCHEME_RE.match(self.args.url):
|
||||||
if os.path.basename(self.env.program_name) == 'https':
|
if os.path.basename(self.env.program_name) == 'https':
|
||||||
scheme = 'https://'
|
scheme = 'https://'
|
||||||
@ -138,18 +194,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
|||||||
else:
|
else:
|
||||||
self.args.url = scheme + self.args.url
|
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):
|
def _setup_standard_streams(self):
|
||||||
"""
|
"""
|
||||||
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
||||||
@ -252,6 +296,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
|||||||
' --ignore-stdin is set.'
|
' --ignore-stdin is set.'
|
||||||
)
|
)
|
||||||
credentials.prompt_password(url.netloc)
|
credentials.prompt_password(url.netloc)
|
||||||
|
|
||||||
|
if (credentials.key and credentials.value):
|
||||||
|
plugin.raw_auth = credentials.key + ":" + credentials.value
|
||||||
|
|
||||||
self.args.auth = plugin.get_auth(
|
self.args.auth = plugin.get_auth(
|
||||||
username=credentials.key,
|
username=credentials.key,
|
||||||
password=credentials.value,
|
password=credentials.value,
|
||||||
@ -361,7 +409,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
|||||||
try:
|
try:
|
||||||
request_items = RequestItems.from_args(
|
request_items = RequestItems.from_args(
|
||||||
request_item_args=self.args.request_items,
|
request_item_args=self.args.request_items,
|
||||||
as_form=self.args.form,
|
request_type=self.args.request_type,
|
||||||
)
|
)
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
if self.args.traceback:
|
if self.args.traceback:
|
||||||
@ -412,8 +460,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
|||||||
self.args.all = True
|
self.args.all = True
|
||||||
|
|
||||||
if self.args.output_options is None:
|
if self.args.output_options is None:
|
||||||
if self.args.verbose:
|
if self.args.verbose >= 2:
|
||||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||||
|
elif self.args.verbose == 1:
|
||||||
|
self.args.output_options = ''.join(BASE_OUTPUT_OPTIONS)
|
||||||
elif self.args.offline:
|
elif self.args.offline:
|
||||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
||||||
elif not self.env.stdout_isatty:
|
elif not self.env.stdout_isatty:
|
||||||
|
@ -57,12 +57,12 @@ class KeyValueArgType:
|
|||||||
|
|
||||||
def __init__(self, *separators: str):
|
def __init__(self, *separators: str):
|
||||||
self.separators = separators
|
self.separators = separators
|
||||||
self.special_characters = set('\\')
|
self.special_characters = set()
|
||||||
for separator in separators:
|
for separator in separators:
|
||||||
self.special_characters.update(separator)
|
self.special_characters.update(separator)
|
||||||
|
|
||||||
def __call__(self, s: str) -> KeyValueArg:
|
def __call__(self, s: str) -> KeyValueArg:
|
||||||
"""Parse raw string arg and return `self.key_value_class` instance.
|
"""Parse raw string arg and return `self.key_value_class` instance.
|
||||||
|
|
||||||
The best of `self.separators` is determined (first found, longest).
|
The best of `self.separators` is determined (first found, longest).
|
||||||
Back slash escaped characters aren't considered as separators
|
Back slash escaped characters aren't considered as separators
|
||||||
@ -113,7 +113,7 @@ class KeyValueArgType:
|
|||||||
There are only two token types - strings and escaped characters:
|
There are only two token types - strings and escaped characters:
|
||||||
|
|
||||||
>>> KeyValueArgType('=').tokenize(r'foo\=bar\\baz')
|
>>> KeyValueArgType('=').tokenize(r'foo\=bar\\baz')
|
||||||
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
|
['foo', Escaped('='), 'bar\\\\baz']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
tokens = ['']
|
tokens = ['']
|
||||||
|
@ -15,6 +15,7 @@ SEPARATOR_HEADER = ':'
|
|||||||
SEPARATOR_HEADER_EMPTY = ';'
|
SEPARATOR_HEADER_EMPTY = ';'
|
||||||
SEPARATOR_CREDENTIALS = ':'
|
SEPARATOR_CREDENTIALS = ':'
|
||||||
SEPARATOR_PROXY = ':'
|
SEPARATOR_PROXY = ':'
|
||||||
|
SEPARATOR_HEADER_EMBED = ':@'
|
||||||
SEPARATOR_DATA_STRING = '='
|
SEPARATOR_DATA_STRING = '='
|
||||||
SEPARATOR_DATA_RAW_JSON = ':='
|
SEPARATOR_DATA_RAW_JSON = ':='
|
||||||
SEPARATOR_FILE_UPLOAD = '@'
|
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_FILE_CONTENTS = '=@'
|
||||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||||
SEPARATOR_QUERY_PARAM = '=='
|
SEPARATOR_QUERY_PARAM = '=='
|
||||||
|
SEPARATOR_QUERY_EMBED_FILE = '==@'
|
||||||
|
|
||||||
# Separators that become request data
|
# Separators that become request data
|
||||||
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
|
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
|
||||||
@ -40,13 +42,17 @@ SEPARATORS_GROUP_MULTIPART = frozenset({
|
|||||||
|
|
||||||
# Separators for items whose value is a filename to be embedded
|
# Separators for items whose value is a filename to be embedded
|
||||||
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
|
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
|
||||||
|
SEPARATOR_HEADER_EMBED,
|
||||||
|
SEPARATOR_QUERY_EMBED_FILE,
|
||||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Separators for raw JSON items
|
# Separators for nested JSON items
|
||||||
SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
SEPARATOR_GROUP_NESTED_JSON_ITEMS = frozenset([
|
||||||
|
SEPARATOR_DATA_STRING,
|
||||||
SEPARATOR_DATA_RAW_JSON,
|
SEPARATOR_DATA_RAW_JSON,
|
||||||
|
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -54,7 +60,9 @@ SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
|||||||
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
|
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
|
||||||
SEPARATOR_HEADER,
|
SEPARATOR_HEADER,
|
||||||
SEPARATOR_HEADER_EMPTY,
|
SEPARATOR_HEADER_EMPTY,
|
||||||
|
SEPARATOR_HEADER_EMBED,
|
||||||
SEPARATOR_QUERY_PARAM,
|
SEPARATOR_QUERY_PARAM,
|
||||||
|
SEPARATOR_QUERY_EMBED_FILE,
|
||||||
SEPARATOR_DATA_STRING,
|
SEPARATOR_DATA_STRING,
|
||||||
SEPARATOR_DATA_RAW_JSON,
|
SEPARATOR_DATA_RAW_JSON,
|
||||||
SEPARATOR_FILE_UPLOAD,
|
SEPARATOR_FILE_UPLOAD,
|
||||||
@ -67,12 +75,18 @@ OUT_REQ_HEAD = 'H'
|
|||||||
OUT_REQ_BODY = 'B'
|
OUT_REQ_BODY = 'B'
|
||||||
OUT_RESP_HEAD = 'h'
|
OUT_RESP_HEAD = 'h'
|
||||||
OUT_RESP_BODY = 'b'
|
OUT_RESP_BODY = 'b'
|
||||||
|
OUT_RESP_META = 'm'
|
||||||
|
|
||||||
OUTPUT_OPTIONS = frozenset({
|
BASE_OUTPUT_OPTIONS = frozenset({
|
||||||
OUT_REQ_HEAD,
|
OUT_REQ_HEAD,
|
||||||
OUT_REQ_BODY,
|
OUT_REQ_BODY,
|
||||||
OUT_RESP_HEAD,
|
OUT_RESP_HEAD,
|
||||||
OUT_RESP_BODY
|
OUT_RESP_BODY,
|
||||||
|
})
|
||||||
|
|
||||||
|
OUTPUT_OPTIONS = frozenset({
|
||||||
|
*BASE_OUTPUT_OPTIONS,
|
||||||
|
OUT_RESP_META,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Pretty
|
# Pretty
|
||||||
@ -111,3 +125,9 @@ class RequestType(enum.Enum):
|
|||||||
FORM = enum.auto()
|
FORM = enum.auto()
|
||||||
MULTIPART = enum.auto()
|
MULTIPART = enum.auto()
|
||||||
JSON = 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,
|
readable_file_arg, response_charset_type, response_mime_type,
|
||||||
)
|
)
|
||||||
from .constants import (
|
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,
|
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,
|
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||||
SORTED_FORMAT_OPTIONS_STRING,
|
SORTED_FORMAT_OPTIONS_STRING,
|
||||||
UNSORTED_FORMAT_OPTIONS_STRING,
|
UNSORTED_FORMAT_OPTIONS_STRING,
|
||||||
)
|
)
|
||||||
|
from .utils import LazyChoices
|
||||||
from ..output.formatters.colors import (
|
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.builtin import BuiltinAuthPlugin
|
||||||
from ..plugins.registry import plugin_manager
|
from ..plugins.registry import plugin_manager
|
||||||
from ..sessions import DEFAULT_SESSIONS_DIR
|
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(
|
parser = HTTPieArgumentParser(
|
||||||
@ -41,6 +42,7 @@ parser = HTTPieArgumentParser(
|
|||||||
|
|
||||||
'''),
|
'''),
|
||||||
)
|
)
|
||||||
|
parser.register('action', 'lazy_choices', LazyChoices)
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
# Positional arguments.
|
# Positional arguments.
|
||||||
@ -118,7 +120,7 @@ positional.add_argument(
|
|||||||
|
|
||||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||||
|
|
||||||
essay=@Documents/essay.txt
|
essay=@Documents/essay.txt
|
||||||
|
|
||||||
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
|
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
|
||||||
|
|
||||||
@ -247,32 +249,38 @@ output_processing.add_argument(
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_style_help(available_styles):
|
||||||
|
return '''
|
||||||
|
Output coloring style (default is "{default}"). It can be one of:
|
||||||
|
|
||||||
|
{available_styles}
|
||||||
|
|
||||||
|
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||||
|
For non-{auto_style} styles to work properly, please make sure that the
|
||||||
|
$TERM environment variable is set to "xterm-256color" or similar
|
||||||
|
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||||
|
'''.format(
|
||||||
|
default=DEFAULT_STYLE,
|
||||||
|
available_styles='\n'.join(
|
||||||
|
f' {line.strip()}'
|
||||||
|
for line in wrap(', '.join(available_styles), 60)
|
||||||
|
).strip(),
|
||||||
|
auto_style=AUTO_STYLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
output_processing.add_argument(
|
output_processing.add_argument(
|
||||||
'--style', '-s',
|
'--style', '-s',
|
||||||
dest='style',
|
dest='style',
|
||||||
metavar='STYLE',
|
metavar='STYLE',
|
||||||
default=DEFAULT_STYLE,
|
default=DEFAULT_STYLE,
|
||||||
choices=sorted(AVAILABLE_STYLES),
|
action='lazy_choices',
|
||||||
help='''
|
getter=get_available_styles,
|
||||||
Output coloring style (default is "{default}"). It can be One of:
|
help_formatter=format_style_help
|
||||||
|
|
||||||
{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,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_sorted_kwargs = {
|
_sorted_kwargs = {
|
||||||
'action': 'append_const',
|
'action': 'append_const',
|
||||||
'const': SORTED_FORMAT_OPTIONS_STRING,
|
'const': SORTED_FORMAT_OPTIONS_STRING,
|
||||||
@ -375,6 +383,7 @@ output_options.add_argument(
|
|||||||
'{OUT_REQ_BODY}' request body
|
'{OUT_REQ_BODY}' request body
|
||||||
'{OUT_RESP_HEAD}' response headers
|
'{OUT_RESP_HEAD}' response headers
|
||||||
'{OUT_RESP_BODY}' response body
|
'{OUT_RESP_BODY}' response body
|
||||||
|
'{OUT_RESP_META}' response metadata
|
||||||
|
|
||||||
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
|
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
|
||||||
headers and body is printed), if standard output is not redirected.
|
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(
|
output_options.add_argument(
|
||||||
'--body', '-b',
|
'--body', '-b',
|
||||||
dest='output_options',
|
dest='output_options',
|
||||||
@ -407,12 +426,16 @@ output_options.add_argument(
|
|||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--verbose', '-v',
|
'--verbose', '-v',
|
||||||
dest='verbose',
|
dest='verbose',
|
||||||
action='store_true',
|
action='count',
|
||||||
|
default=0,
|
||||||
help=f'''
|
help=f'''
|
||||||
Verbose output. Print the whole request as well as the response. Also print
|
Verbose output. For the level one (with single `-v`/`--verbose`), print
|
||||||
any intermediary requests/responses (such as redirects).
|
the whole request as well as the response. Also print any intermediary
|
||||||
It's a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
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(
|
output_options.add_argument(
|
||||||
@ -497,12 +520,14 @@ output_options.add_argument(
|
|||||||
|
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--quiet', '-q',
|
'--quiet', '-q',
|
||||||
action='store_true',
|
action='count',
|
||||||
default=False,
|
default=0,
|
||||||
help='''
|
help='''
|
||||||
Do not print to stdout or stderr.
|
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||||
|
Provide twice to suppress warnings as well.
|
||||||
stdout is still redirected if --output is specified.
|
stdout is still redirected if --output is specified.
|
||||||
Flag doesn't affect behaviour of download beyond not printing to terminal.
|
Flag doesn't affect behaviour of download beyond not printing to terminal.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -552,36 +577,24 @@ auth = parser.add_argument_group(title='Authentication')
|
|||||||
auth.add_argument(
|
auth.add_argument(
|
||||||
'--auth', '-a',
|
'--auth', '-a',
|
||||||
default=None,
|
default=None,
|
||||||
metavar='USER[:PASS]',
|
metavar='USER[:PASS] | TOKEN',
|
||||||
help='''
|
help='''
|
||||||
If only the username is provided (-a username), HTTPie will prompt
|
For username/password based authentication mechanisms (e.g
|
||||||
for the password.
|
basic auth or digest auth) if only the username is provided
|
||||||
|
(-a username), HTTPie will prompt for the password.
|
||||||
|
|
||||||
''',
|
''',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class _AuthTypeLazyChoices:
|
def format_auth_help(auth_plugins_mapping):
|
||||||
# Needed for plugin testing
|
auth_plugins = list(auth_plugins_mapping.values())
|
||||||
|
return '''
|
||||||
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='''
|
|
||||||
The authentication mechanism to be used. Defaults to "{default}".
|
The authentication mechanism to be used. Defaults to "{default}".
|
||||||
|
|
||||||
{types}
|
{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}": {name}{package}{description}'.format(
|
||||||
type=plugin.auth_type,
|
type=plugin.auth_type,
|
||||||
name=plugin.name,
|
name=plugin.name,
|
||||||
@ -594,8 +607,18 @@ auth.add_argument(
|
|||||||
'\n ' + ('\n '.join(wrap(plugin.description)))
|
'\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(
|
auth.add_argument(
|
||||||
'--ignore-netrc',
|
'--ignore-netrc',
|
||||||
|
@ -1,15 +1,41 @@
|
|||||||
from collections import OrderedDict
|
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):
|
class RequestJSONDataDict(OrderedDict):
|
||||||
pass
|
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 os
|
||||||
|
import functools
|
||||||
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
|
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from .argtypes import KeyValueArg
|
from .argtypes import KeyValueArg
|
||||||
from .constants import (
|
from .constants import (
|
||||||
SEPARATORS_GROUP_MULTIPART, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
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_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
|
||||||
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
||||||
SEPARATOR_QUERY_PARAM,
|
SEPARATOR_HEADER_EMBED, SEPARATOR_QUERY_PARAM,
|
||||||
|
SEPARATOR_QUERY_EMBED_FILE, RequestType
|
||||||
)
|
)
|
||||||
from .dicts import (
|
from .dicts import (
|
||||||
MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
|
BaseMultiDict, MultipartRequestDataDict, RequestDataDict,
|
||||||
RequestHeadersDict, RequestJSONDataDict,
|
RequestFilesDict, HTTPHeadersDict, RequestJSONDataDict,
|
||||||
RequestQueryParamsDict,
|
RequestQueryParamsDict,
|
||||||
)
|
)
|
||||||
from .exceptions import ParseError
|
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:
|
class RequestItems:
|
||||||
|
|
||||||
def __init__(self, as_form=False):
|
def __init__(self, request_type: Optional[RequestType] = None):
|
||||||
self.headers = RequestHeadersDict()
|
self.headers = HTTPHeadersDict()
|
||||||
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
|
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.files = RequestFilesDict()
|
||||||
self.params = RequestQueryParamsDict()
|
self.params = RequestQueryParamsDict()
|
||||||
# To preserve the order of fields in file upload multipart requests.
|
# To preserve the order of fields in file upload multipart requests.
|
||||||
@ -32,9 +37,9 @@ class RequestItems:
|
|||||||
def from_args(
|
def from_args(
|
||||||
cls,
|
cls,
|
||||||
request_item_args: List[KeyValueArg],
|
request_item_args: List[KeyValueArg],
|
||||||
as_form=False,
|
request_type: Optional[RequestType] = None,
|
||||||
) -> 'RequestItems':
|
) -> 'RequestItems':
|
||||||
instance = cls(as_form=as_form)
|
instance = cls(request_type=request_type)
|
||||||
rules: Dict[str, Tuple[Callable, dict]] = {
|
rules: Dict[str, Tuple[Callable, dict]] = {
|
||||||
SEPARATOR_HEADER: (
|
SEPARATOR_HEADER: (
|
||||||
process_header_arg,
|
process_header_arg,
|
||||||
@ -44,10 +49,18 @@ class RequestItems:
|
|||||||
process_empty_header_arg,
|
process_empty_header_arg,
|
||||||
instance.headers,
|
instance.headers,
|
||||||
),
|
),
|
||||||
|
SEPARATOR_HEADER_EMBED: (
|
||||||
|
process_embed_header_arg,
|
||||||
|
instance.headers,
|
||||||
|
),
|
||||||
SEPARATOR_QUERY_PARAM: (
|
SEPARATOR_QUERY_PARAM: (
|
||||||
process_query_param_arg,
|
process_query_param_arg,
|
||||||
instance.params,
|
instance.params,
|
||||||
),
|
),
|
||||||
|
SEPARATOR_QUERY_EMBED_FILE: (
|
||||||
|
process_embed_query_param_arg,
|
||||||
|
instance.params,
|
||||||
|
),
|
||||||
SEPARATOR_FILE_UPLOAD: (
|
SEPARATOR_FILE_UPLOAD: (
|
||||||
process_file_upload_arg,
|
process_file_upload_arg,
|
||||||
instance.files,
|
instance.files,
|
||||||
@ -60,24 +73,47 @@ class RequestItems:
|
|||||||
process_data_embed_file_contents_arg,
|
process_data_embed_file_contents_arg,
|
||||||
instance.data,
|
instance.data,
|
||||||
),
|
),
|
||||||
|
SEPARATOR_GROUP_NESTED_JSON_ITEMS: (
|
||||||
|
process_data_nested_json_embed_args,
|
||||||
|
instance.data,
|
||||||
|
),
|
||||||
SEPARATOR_DATA_RAW_JSON: (
|
SEPARATOR_DATA_RAW_JSON: (
|
||||||
process_data_raw_json_embed_arg,
|
json_only(instance, process_data_raw_json_embed_arg),
|
||||||
instance.data,
|
instance.data,
|
||||||
),
|
),
|
||||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
|
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,
|
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:
|
for arg in request_item_args:
|
||||||
processor_func, target_dict = rules[arg.sep]
|
processor_func, target_dict = rules[arg.sep]
|
||||||
value = processor_func(arg)
|
value = processor_func(arg)
|
||||||
target_dict[arg.key] = value
|
|
||||||
|
|
||||||
if arg.sep in SEPARATORS_GROUP_MULTIPART:
|
if arg.sep in SEPARATORS_GROUP_MULTIPART:
|
||||||
instance.multipart_data[arg.key] = value
|
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
|
return instance
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +124,10 @@ def process_header_arg(arg: KeyValueArg) -> Optional[str]:
|
|||||||
return arg.value or None
|
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:
|
def process_empty_header_arg(arg: KeyValueArg) -> str:
|
||||||
if not arg.value:
|
if not arg.value:
|
||||||
return arg.value
|
return arg.value
|
||||||
@ -100,6 +140,10 @@ def process_query_param_arg(arg: KeyValueArg) -> str:
|
|||||||
return arg.value
|
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]:
|
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||||
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
||||||
filename = parts[0]
|
filename = parts[0]
|
||||||
@ -123,6 +167,29 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
|
|||||||
return load_text_file(arg)
|
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:
|
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
|
||||||
contents = load_text_file(arg)
|
contents = load_text_file(arg)
|
||||||
value = load_json(arg, contents)
|
value = load_json(arg, contents)
|
||||||
@ -134,6 +201,10 @@ def process_data_raw_json_embed_arg(arg: KeyValueArg) -> JSONType:
|
|||||||
return value
|
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:
|
def load_text_file(item: KeyValueArg) -> str:
|
||||||
path = item.value
|
path = item.value
|
||||||
try:
|
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,21 @@ import http.client
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from typing import Any, Dict, Callable, Iterable
|
||||||
from typing import Callable, Iterable, Union
|
|
||||||
from urllib.parse import urlparse, urlunparse
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
import urllib3
|
import urllib3
|
||||||
from . import __version__
|
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 .encoding import UTF8
|
||||||
|
from .models import RequestsMessage
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
from .sessions import get_httpie_session
|
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 (
|
from .uploads import (
|
||||||
compress_request, prepare_request_body,
|
compress_request, prepare_request_body,
|
||||||
get_multipart_data_and_content_type,
|
get_multipart_data_and_content_type,
|
||||||
@ -32,15 +34,15 @@ DEFAULT_UA = f'HTTPie/{__version__}'
|
|||||||
|
|
||||||
|
|
||||||
def collect_messages(
|
def collect_messages(
|
||||||
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
config_dir: Path,
|
|
||||||
request_body_read_callback: Callable[[bytes], None] = None,
|
request_body_read_callback: Callable[[bytes], None] = None,
|
||||||
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
|
) -> Iterable[RequestsMessage]:
|
||||||
httpie_session = None
|
httpie_session = None
|
||||||
httpie_session_headers = None
|
httpie_session_headers = None
|
||||||
if args.session or args.session_read_only:
|
if args.session or args.session_read_only:
|
||||||
httpie_session = get_httpie_session(
|
httpie_session = get_httpie_session(
|
||||||
config_dir=config_dir,
|
config_dir=env.config.directory,
|
||||||
session_name=args.session or args.session_read_only,
|
session_name=args.session or args.session_read_only,
|
||||||
host=args.headers.get('Host'),
|
host=args.headers.get('Host'),
|
||||||
url=args.url,
|
url=args.url,
|
||||||
@ -48,6 +50,7 @@ def collect_messages(
|
|||||||
httpie_session_headers = httpie_session.headers
|
httpie_session_headers = httpie_session.headers
|
||||||
|
|
||||||
request_kwargs = make_request_kwargs(
|
request_kwargs = make_request_kwargs(
|
||||||
|
env,
|
||||||
args=args,
|
args=args,
|
||||||
base_headers=httpie_session_headers,
|
base_headers=httpie_session_headers,
|
||||||
request_body_read_callback=request_body_read_callback
|
request_body_read_callback=request_body_read_callback
|
||||||
@ -79,6 +82,7 @@ def collect_messages(
|
|||||||
|
|
||||||
request = requests.Request(**request_kwargs)
|
request = requests.Request(**request_kwargs)
|
||||||
prepared_request = requests_session.prepare_request(request)
|
prepared_request = requests_session.prepare_request(request)
|
||||||
|
apply_missing_repeated_headers(prepared_request, request.headers)
|
||||||
if args.path_as_is:
|
if args.path_as_is:
|
||||||
prepared_request.url = ensure_path_as_is(
|
prepared_request.url = ensure_path_as_is(
|
||||||
orig_url=args.url,
|
orig_url=args.url,
|
||||||
@ -152,6 +156,7 @@ def build_requests_session(
|
|||||||
requests_session = requests.Session()
|
requests_session = requests.Session()
|
||||||
|
|
||||||
# Install our adapter.
|
# Install our adapter.
|
||||||
|
http_adapter = HTTPieHTTPAdapter()
|
||||||
https_adapter = HTTPieHTTPSAdapter(
|
https_adapter = HTTPieHTTPSAdapter(
|
||||||
ciphers=ciphers,
|
ciphers=ciphers,
|
||||||
verify=verify,
|
verify=verify,
|
||||||
@ -160,6 +165,7 @@ def build_requests_session(
|
|||||||
if ssl_version else None
|
if ssl_version else None
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
requests_session.mount('http://', http_adapter)
|
||||||
requests_session.mount('https://', https_adapter)
|
requests_session.mount('https://', https_adapter)
|
||||||
|
|
||||||
# Install adapters from plugins.
|
# Install adapters from plugins.
|
||||||
@ -178,8 +184,8 @@ def dump_request(kwargs: dict):
|
|||||||
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
|
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
|
||||||
|
|
||||||
|
|
||||||
def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
|
def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
|
||||||
final_headers = RequestHeadersDict()
|
final_headers = HTTPHeadersDict()
|
||||||
for name, value in headers.items():
|
for name, value in headers.items():
|
||||||
if value is not None:
|
if value is not None:
|
||||||
# “leading or trailing LWS MAY be removed without
|
# “leading or trailing LWS MAY be removed without
|
||||||
@ -190,12 +196,42 @@ def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
|
|||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
# See <https://github.com/httpie/httpie/issues/212>
|
# See <https://github.com/httpie/httpie/issues/212>
|
||||||
value = value.encode()
|
value = value.encode()
|
||||||
final_headers[name] = value
|
final_headers.add(name, value)
|
||||||
return final_headers
|
return final_headers
|
||||||
|
|
||||||
|
|
||||||
def make_default_headers(args: argparse.Namespace) -> RequestHeadersDict:
|
def apply_missing_repeated_headers(
|
||||||
default_headers = RequestHeadersDict({
|
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
|
'User-Agent': DEFAULT_UA
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -238,9 +274,28 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def json_dict_to_request_body(data: Dict[str, Any]) -> str:
|
||||||
|
# Propagate the top-level list if there is only one
|
||||||
|
# item in the object, with an en empty key.
|
||||||
|
if len(data) == 1:
|
||||||
|
[(key, value)] = data.items()
|
||||||
|
if key == '' and isinstance(value, list):
|
||||||
|
data = value
|
||||||
|
|
||||||
|
if data:
|
||||||
|
data = json.dumps(data)
|
||||||
|
else:
|
||||||
|
# We need to set data to an empty string to prevent requests
|
||||||
|
# from assigning an empty list to `response.request.data`.
|
||||||
|
data = ''
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def make_request_kwargs(
|
def make_request_kwargs(
|
||||||
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
base_headers: RequestHeadersDict = None,
|
base_headers: HTTPHeadersDict = None,
|
||||||
request_body_read_callback=lambda chunk: chunk
|
request_body_read_callback=lambda chunk: chunk
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
@ -252,12 +307,7 @@ def make_request_kwargs(
|
|||||||
data = args.data
|
data = args.data
|
||||||
auto_json = data and not args.form
|
auto_json = data and not args.form
|
||||||
if (args.json or auto_json) and isinstance(data, dict):
|
if (args.json or auto_json) and isinstance(data, dict):
|
||||||
if data:
|
data = json_dict_to_request_body(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 = ''
|
|
||||||
|
|
||||||
# Finalize headers.
|
# Finalize headers.
|
||||||
headers = make_default_headers(args)
|
headers = make_default_headers(args)
|
||||||
@ -282,7 +332,8 @@ def make_request_kwargs(
|
|||||||
'url': args.url,
|
'url': args.url,
|
||||||
'headers': headers,
|
'headers': headers,
|
||||||
'data': prepare_request_body(
|
'data': prepare_request_body(
|
||||||
body=data,
|
env,
|
||||||
|
data,
|
||||||
body_read_callback=request_body_read_callback,
|
body_read_callback=request_body_read_callback,
|
||||||
chunked=args.chunked,
|
chunked=args.chunked,
|
||||||
offline=args.offline,
|
offline=args.offline,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from typing import Any, Optional, Iterable
|
||||||
|
|
||||||
|
|
||||||
is_windows = 'win32' in str(sys.platform).lower()
|
is_windows = 'win32' in str(sys.platform).lower()
|
||||||
@ -52,3 +53,38 @@ except ImportError:
|
|||||||
return self
|
return self
|
||||||
res = instance.__dict__[self.name] = self.func(instance)
|
res = instance.__dict__[self.name] = self.func(instance)
|
||||||
return res
|
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
|
@property
|
||||||
def default_options(self) -> list:
|
def default_options(self) -> list:
|
||||||
return self['default_options']
|
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 sys
|
||||||
import os
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, Optional
|
from typing import Iterator, IO, Optional
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -120,6 +121,19 @@ class Environment:
|
|||||||
self._devnull = open(os.devnull, 'w+')
|
self._devnull = open(os.devnull, 'w+')
|
||||||
return self._devnull
|
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'):
|
def log_error(self, msg, level='error'):
|
||||||
assert level in ['error', 'warning']
|
assert level in ['error', 'warning']
|
||||||
self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')
|
self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')
|
||||||
|
141
httpie/core.py
141
httpie/core.py
@ -2,39 +2,40 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Optional, Tuple, Union
|
import socket
|
||||||
|
from typing import List, Optional, Union, Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pygments import __version__ as pygments_version
|
from pygments import __version__ as pygments_version
|
||||||
from requests import __version__ as requests_version
|
from requests import __version__ as requests_version
|
||||||
|
|
||||||
from . import __version__ as httpie_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 .client import collect_messages
|
||||||
from .context import Environment
|
from .context import Environment
|
||||||
from .downloads import Downloader
|
from .downloads import Downloader
|
||||||
|
from .models import (
|
||||||
|
RequestsMessageKind,
|
||||||
|
OutputOptions,
|
||||||
|
)
|
||||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
from .status import ExitStatus, http_status_to_exit_status
|
from .status import ExitStatus, http_status_to_exit_status
|
||||||
|
from .utils import unwrap_context
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyDefaultArgument
|
# noinspection PyDefaultArgument
|
||||||
def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitStatus:
|
def raw_main(
|
||||||
"""
|
parser: argparse.ArgumentParser,
|
||||||
The main function.
|
main_program: Callable[[argparse.Namespace, Environment], ExitStatus],
|
||||||
|
args: List[Union[str, bytes]] = sys.argv,
|
||||||
Pre-process args, handle some special types of invocations,
|
env: Environment = Environment()
|
||||||
and run the main program with error handling.
|
) -> ExitStatus:
|
||||||
|
|
||||||
Return exit status code.
|
|
||||||
|
|
||||||
"""
|
|
||||||
program_name, *args = args
|
program_name, *args = args
|
||||||
env.program_name = os.path.basename(program_name)
|
env.program_name = os.path.basename(program_name)
|
||||||
args = decode_raw_args(args, env.stdin_encoding)
|
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 env.config.default_options:
|
||||||
args = env.config.default_options + args
|
args = env.config.default_options + args
|
||||||
@ -42,6 +43,21 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
|||||||
include_debug_info = '--debug' in args
|
include_debug_info = '--debug' in args
|
||||||
include_traceback = include_debug_info or '--traceback' 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:
|
if include_debug_info:
|
||||||
print_debug_info(env)
|
print_debug_info(env)
|
||||||
if args == ['--debug']:
|
if args == ['--debug']:
|
||||||
@ -54,6 +70,11 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
|||||||
args=args,
|
args=args,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
except HTTPieSyntaxError as exc:
|
||||||
|
env.stderr.write(str(exc) + "\n")
|
||||||
|
if include_traceback:
|
||||||
|
raise
|
||||||
|
exit_status = ExitStatus.ERROR
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
env.stderr.write('\n')
|
env.stderr.write('\n')
|
||||||
if include_traceback:
|
if include_traceback:
|
||||||
@ -67,7 +88,7 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
|||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
exit_status = program(
|
exit_status = main_program(
|
||||||
args=parsed_args,
|
args=parsed_args,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
@ -91,38 +112,50 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
|
|||||||
f'Too many redirects'
|
f'Too many redirects'
|
||||||
f' (--max-redirects={parsed_args.max_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:
|
except Exception as e:
|
||||||
# TODO: Further distinction between expected and unexpected errors.
|
# TODO: Further distinction between expected and unexpected errors.
|
||||||
msg = str(e)
|
handle_generic_error(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
|
|
||||||
exit_status = ExitStatus.ERROR
|
exit_status = ExitStatus.ERROR
|
||||||
|
|
||||||
return exit_status
|
return exit_status
|
||||||
|
|
||||||
|
|
||||||
def get_output_options(
|
def main(
|
||||||
args: argparse.Namespace,
|
args: List[Union[str, bytes]] = sys.argv,
|
||||||
message: Union[requests.PreparedRequest, requests.Response]
|
env: Environment = Environment()
|
||||||
) -> Tuple[bool, bool]:
|
) -> ExitStatus:
|
||||||
return {
|
"""
|
||||||
requests.PreparedRequest: (
|
The main function.
|
||||||
OUT_REQ_HEAD in args.output_options,
|
|
||||||
OUT_REQ_BODY in args.output_options,
|
Pre-process args, handle some special types of invocations,
|
||||||
),
|
and run the main program with error handling.
|
||||||
requests.Response: (
|
|
||||||
OUT_RESP_HEAD in args.output_options,
|
Return exit status code.
|
||||||
OUT_RESP_BODY in args.output_options,
|
|
||||||
),
|
"""
|
||||||
}[type(message)]
|
|
||||||
|
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:
|
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||||
@ -153,31 +186,32 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
msg.is_body_upload_chunk = True
|
msg.is_body_upload_chunk = True
|
||||||
msg.body = chunk
|
msg.body = chunk
|
||||||
msg.headers = initial_request.headers
|
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:
|
try:
|
||||||
if args.download:
|
if args.download:
|
||||||
args.follow = True # --download implies --follow.
|
args.follow = True # --download implies --follow.
|
||||||
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
||||||
downloader.pre_request(args.headers)
|
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)
|
request_body_read_callback=request_body_read_callback)
|
||||||
force_separator = False
|
force_separator = False
|
||||||
prev_with_body = False
|
prev_with_body = False
|
||||||
|
|
||||||
# Process messages as they’re generated
|
# Process messages as they’re generated
|
||||||
for message in messages:
|
for message in messages:
|
||||||
is_request = isinstance(message, requests.PreparedRequest)
|
output_options = OutputOptions.from_message(message, args.output_options)
|
||||||
with_headers, with_body = get_output_options(args=args, message=message)
|
|
||||||
do_write_body = with_body
|
do_write_body = output_options.body
|
||||||
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
|
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 after a previous message with body, if needed. See test_tokens.py.
|
||||||
separate()
|
separate()
|
||||||
force_separator = False
|
force_separator = False
|
||||||
if is_request:
|
if output_options.kind is RequestsMessageKind.REQUEST:
|
||||||
if not initial_request:
|
if not initial_request:
|
||||||
initial_request = message
|
initial_request = message
|
||||||
if with_body:
|
if output_options.body:
|
||||||
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
||||||
do_write_body = not is_streamed_upload
|
do_write_body = not is_streamed_upload
|
||||||
force_separator = is_streamed_upload and env.stdout_isatty
|
force_separator = is_streamed_upload and env.stdout_isatty
|
||||||
@ -185,11 +219,12 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
final_response = message
|
final_response = message
|
||||||
if args.check_status or downloader:
|
if args.check_status or downloader:
|
||||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet):
|
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
|
||||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
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,
|
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
|
||||||
with_body=do_write_body)
|
body=do_write_body
|
||||||
prev_with_body = with_body
|
))
|
||||||
|
prev_with_body = output_options.body
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
if force_separator:
|
if force_separator:
|
||||||
|
@ -14,7 +14,7 @@ from urllib.parse import urlsplit
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .models import HTTPResponse
|
from .models import HTTPResponse, OutputOptions
|
||||||
from .output.streams import RawStream
|
from .output.streams import RawStream
|
||||||
from .utils import humanize_bytes
|
from .utils import humanize_bytes
|
||||||
|
|
||||||
@ -266,12 +266,11 @@ class Downloader:
|
|||||||
total_size=total_size
|
total_size=total_size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
||||||
stream = RawStream(
|
stream = RawStream(
|
||||||
msg=HTTPResponse(final_response),
|
msg=HTTPResponse(final_response),
|
||||||
with_headers=False,
|
output_options=output_options,
|
||||||
with_body=True,
|
|
||||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||||
chunk_size=1024 * 8
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._progress_reporter.output.write(
|
self._progress_reporter.output.write(
|
||||||
@ -324,7 +323,7 @@ class Downloader:
|
|||||||
content_type=final_response.headers.get('Content-Type'),
|
content_type=final_response.headers.get('Content-Type'),
|
||||||
)
|
)
|
||||||
unique_filename = get_unique_filename(filename)
|
unique_filename = get_unique_filename(filename)
|
||||||
return open(unique_filename, mode='a+b')
|
return open(unique_filename, buffering=0, mode='a+b')
|
||||||
|
|
||||||
|
|
||||||
class DownloadStatus:
|
class DownloadStatus:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Union
|
from typing import Union, Tuple
|
||||||
|
|
||||||
from charset_normalizer import from_bytes
|
from charset_normalizer import from_bytes
|
||||||
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
|
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
|
||||||
@ -29,7 +29,7 @@ def detect_encoding(content: ContentBytes) -> str:
|
|||||||
return encoding
|
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`.
|
"""Decode `content` using the given `encoding`.
|
||||||
If no `encoding` is provided, the best effort is to guess it from `content`.
|
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:
|
if not encoding:
|
||||||
encoding = detect_encoding(content)
|
encoding = detect_encoding(content)
|
||||||
return content.decode(encoding, 'replace')
|
return content.decode(encoding, 'replace'), encoding
|
||||||
|
|
||||||
|
|
||||||
def smart_encode(content: str, encoding: str) -> bytes:
|
def smart_encode(content: str, encoding: str) -> bytes:
|
||||||
|
0
httpie/manager/__init__.py
Normal file
0
httpie/manager/__init__.py
Normal file
61
httpie/manager/__main__.py
Normal file
61
httpie/manager/__main__.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from httpie.context import Environment
|
||||||
|
from httpie.status import ExitStatus
|
||||||
|
from httpie.manager.cli import parser
|
||||||
|
from httpie.manager.core import MSG_COMMAND_CONFUSION, program as main_program
|
||||||
|
|
||||||
|
|
||||||
|
def is_http_command(args: List[Union[str, bytes]], env: Environment) -> bool:
|
||||||
|
"""Check whether http/https parser can parse the arguments."""
|
||||||
|
|
||||||
|
from httpie.cli.definition import parser as http_parser
|
||||||
|
from httpie.manager.cli import COMMANDS
|
||||||
|
|
||||||
|
# If the user already selected a top-level sub-command, never
|
||||||
|
# show the http/https version. E.g httpie plugins pie.dev/post
|
||||||
|
if len(args) >= 1 and args[0] in COMMANDS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with env.as_silent():
|
||||||
|
try:
|
||||||
|
http_parser.parse_args(env=env, args=args)
|
||||||
|
except (Exception, SystemExit):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()) -> ExitStatus:
|
||||||
|
from httpie.core import raw_main
|
||||||
|
|
||||||
|
try:
|
||||||
|
return raw_main(
|
||||||
|
parser=parser,
|
||||||
|
main_program=main_program,
|
||||||
|
args=args,
|
||||||
|
env=env
|
||||||
|
)
|
||||||
|
except argparse.ArgumentError:
|
||||||
|
program_args = args[1:]
|
||||||
|
if is_http_command(program_args, env):
|
||||||
|
env.stderr.write(MSG_COMMAND_CONFUSION.format(args=' '.join(program_args)) + "\n")
|
||||||
|
|
||||||
|
return ExitStatus.ERROR
|
||||||
|
|
||||||
|
|
||||||
|
def program():
|
||||||
|
try:
|
||||||
|
exit_status = main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
from httpie.status import ExitStatus
|
||||||
|
exit_status = ExitStatus.ERROR_CTRL_C
|
||||||
|
|
||||||
|
return exit_status
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': # pragma: nocover
|
||||||
|
sys.exit(program())
|
112
httpie/manager/cli.py
Normal file
112
httpie/manager/cli.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
from textwrap import dedent
|
||||||
|
from httpie.cli.argparser import HTTPieManagerArgumentParser
|
||||||
|
from httpie import __version__
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
'plugins': {
|
||||||
|
'help': 'Manage HTTPie plugins.',
|
||||||
|
'install': [
|
||||||
|
'Install the given targets from PyPI '
|
||||||
|
'or from a local paths.',
|
||||||
|
{
|
||||||
|
'dest': 'targets',
|
||||||
|
'nargs': '+',
|
||||||
|
'help': 'targets to install'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'upgrade': [
|
||||||
|
'Upgrade the given plugins',
|
||||||
|
{
|
||||||
|
'dest': 'targets',
|
||||||
|
'nargs': '+',
|
||||||
|
'help': 'targets to upgrade'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'uninstall': [
|
||||||
|
'Uninstall the given HTTPie plugins.',
|
||||||
|
{
|
||||||
|
'dest': 'targets',
|
||||||
|
'nargs': '+',
|
||||||
|
'help': 'targets to install'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'list': [
|
||||||
|
'List all installed HTTPie plugins.'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def missing_subcommand(*args) -> str:
|
||||||
|
base = COMMANDS
|
||||||
|
for arg in args:
|
||||||
|
base = base[arg]
|
||||||
|
|
||||||
|
assert isinstance(base, dict)
|
||||||
|
subcommands = ', '.join(map(repr, base.keys()))
|
||||||
|
return f'Please specify one of these: {subcommands}'
|
||||||
|
|
||||||
|
|
||||||
|
def generate_subparsers(root, parent_parser, definitions):
|
||||||
|
action_dest = '_'.join(parent_parser.prog.split()[1:] + ['action'])
|
||||||
|
actions = parent_parser.add_subparsers(
|
||||||
|
dest=action_dest
|
||||||
|
)
|
||||||
|
for command, properties in definitions.items():
|
||||||
|
is_subparser = isinstance(properties, dict)
|
||||||
|
descr = properties.pop('help', None) if is_subparser else properties.pop(0)
|
||||||
|
command_parser = actions.add_parser(command, description=descr)
|
||||||
|
command_parser.root = root
|
||||||
|
if is_subparser:
|
||||||
|
generate_subparsers(root, command_parser, properties)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for argument in properties:
|
||||||
|
command_parser.add_argument(**argument)
|
||||||
|
|
||||||
|
|
||||||
|
parser = HTTPieManagerArgumentParser(
|
||||||
|
prog='httpie',
|
||||||
|
description=dedent(
|
||||||
|
'''
|
||||||
|
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
|
||||||
|
|
||||||
|
Be aware that you might be looking for http/https commands for sending
|
||||||
|
HTTP requests. This command is only available for managing the HTTTPie
|
||||||
|
plugins and the configuration around it.
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='''
|
||||||
|
Prints the exception traceback should one occur, as well as other
|
||||||
|
information useful for debugging HTTPie itself and for reporting bugs.
|
||||||
|
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--traceback',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='''
|
||||||
|
Prints the exception traceback should one occur.
|
||||||
|
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version=__version__,
|
||||||
|
help='''
|
||||||
|
Show version and exit.
|
||||||
|
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_subparsers(parser, parser, COMMANDS)
|
33
httpie/manager/core.py
Normal file
33
httpie/manager/core.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
from httpie.context import Environment
|
||||||
|
from httpie.manager.plugins import PluginInstaller
|
||||||
|
from httpie.status import ExitStatus
|
||||||
|
from httpie.manager.cli import missing_subcommand, parser
|
||||||
|
|
||||||
|
MSG_COMMAND_CONFUSION = '''\
|
||||||
|
This command is only for managing HTTPie plugins.
|
||||||
|
To send a request, please use the http/https commands:
|
||||||
|
|
||||||
|
$ http {args}
|
||||||
|
|
||||||
|
$ https {args}
|
||||||
|
'''
|
||||||
|
|
||||||
|
# noinspection PyStringFormat
|
||||||
|
MSG_NAKED_INVOCATION = f'''\
|
||||||
|
{missing_subcommand()}
|
||||||
|
|
||||||
|
{MSG_COMMAND_CONFUSION}
|
||||||
|
'''.rstrip("\n").format(args='POST pie.dev/post hello=world')
|
||||||
|
|
||||||
|
|
||||||
|
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||||
|
if args.action is None:
|
||||||
|
parser.error(MSG_NAKED_INVOCATION)
|
||||||
|
|
||||||
|
if args.action == 'plugins':
|
||||||
|
plugins = PluginInstaller(env, debug=args.debug)
|
||||||
|
return plugins.run(args.plugins_action, args)
|
||||||
|
|
||||||
|
return ExitStatus.SUCCESS
|
250
httpie/manager/plugins.py
Normal file
250
httpie/manager/plugins.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from collections import defaultdict
|
||||||
|
from contextlib import suppress
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, Optional, List
|
||||||
|
|
||||||
|
from httpie.manager.cli import parser, missing_subcommand
|
||||||
|
from httpie.compat import importlib_metadata, get_dist_name
|
||||||
|
from httpie.context import Environment
|
||||||
|
from httpie.status import ExitStatus
|
||||||
|
from httpie.utils import as_site
|
||||||
|
|
||||||
|
PEP_503 = re.compile(r"[-_.]+")
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstaller:
|
||||||
|
|
||||||
|
def __init__(self, env: Environment, debug: bool = False) -> None:
|
||||||
|
self.env = env
|
||||||
|
self.dir = env.config.plugins_dir
|
||||||
|
self.debug = debug
|
||||||
|
|
||||||
|
self.setup_plugins_dir()
|
||||||
|
|
||||||
|
def setup_plugins_dir(self) -> None:
|
||||||
|
try:
|
||||||
|
self.dir.mkdir(
|
||||||
|
exist_ok=True,
|
||||||
|
parents=True
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
self.env.stderr.write(
|
||||||
|
f'Couldn\'t create "{self.dir!s}"'
|
||||||
|
' directory for plugin installation.'
|
||||||
|
' Please re-check the permissions for that directory,'
|
||||||
|
' and if needed, allow write-access.'
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def fail(
|
||||||
|
self,
|
||||||
|
command: str,
|
||||||
|
target: Optional[str] = None,
|
||||||
|
reason: Optional[str] = None
|
||||||
|
) -> ExitStatus:
|
||||||
|
message = f'Can\'t {command}'
|
||||||
|
if target:
|
||||||
|
message += f' {target!r}'
|
||||||
|
if reason:
|
||||||
|
message += f': {reason}'
|
||||||
|
|
||||||
|
self.env.stderr.write(message + '\n')
|
||||||
|
return ExitStatus.ERROR
|
||||||
|
|
||||||
|
def pip(self, *args, **kwargs) -> subprocess.CompletedProcess:
|
||||||
|
options = {
|
||||||
|
'check': True,
|
||||||
|
'shell': False,
|
||||||
|
'stdout': self.env.stdout,
|
||||||
|
'stderr': subprocess.PIPE,
|
||||||
|
}
|
||||||
|
options.update(kwargs)
|
||||||
|
|
||||||
|
cmd = [sys.executable, '-m', 'pip', *args]
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
**options
|
||||||
|
)
|
||||||
|
|
||||||
|
def _install(self, targets: List[str], mode='install', **process_options) -> Tuple[
|
||||||
|
Optional[bytes], ExitStatus
|
||||||
|
]:
|
||||||
|
pip_args = [
|
||||||
|
'install',
|
||||||
|
f'--prefix={self.dir}',
|
||||||
|
'--no-warn-script-location',
|
||||||
|
]
|
||||||
|
if mode == 'upgrade':
|
||||||
|
pip_args.append('--upgrade')
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = self.pip(
|
||||||
|
*pip_args,
|
||||||
|
*targets,
|
||||||
|
**process_options,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
reason = None
|
||||||
|
if error.stderr:
|
||||||
|
stderr = error.stderr.decode()
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
self.env.stderr.write('Command failed: ')
|
||||||
|
self.env.stderr.write(' '.join(error.cmd) + '\n')
|
||||||
|
self.env.stderr.write(textwrap.indent(' ', stderr))
|
||||||
|
|
||||||
|
last_line = stderr.strip().splitlines()[-1]
|
||||||
|
severity, _, message = last_line.partition(': ')
|
||||||
|
if severity == 'ERROR':
|
||||||
|
reason = message
|
||||||
|
|
||||||
|
stdout = error.stdout
|
||||||
|
exit_status = self.fail(mode, ', '.join(targets), reason)
|
||||||
|
else:
|
||||||
|
stdout = process.stdout
|
||||||
|
exit_status = ExitStatus.SUCCESS
|
||||||
|
|
||||||
|
return stdout, exit_status
|
||||||
|
|
||||||
|
def install(self, targets: List[str]) -> ExitStatus:
|
||||||
|
self.env.stdout.write(f"Installing {', '.join(targets)}...\n")
|
||||||
|
self.env.stdout.flush()
|
||||||
|
_, exit_status = self._install(targets)
|
||||||
|
return exit_status
|
||||||
|
|
||||||
|
def _clear_metadata(self, targets: List[str]) -> None:
|
||||||
|
# Due to an outstanding pip problem[0], we have to get rid of
|
||||||
|
# existing metadata for old versions manually.
|
||||||
|
# [0]: https://github.com/pypa/pip/issues/10727
|
||||||
|
result_deps = defaultdict(list)
|
||||||
|
for child in as_site(self.dir).iterdir():
|
||||||
|
if child.suffix in {'.dist-info', '.egg-info'}:
|
||||||
|
name, _, version = child.stem.rpartition('-')
|
||||||
|
result_deps[name].append((version, child))
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
name, _, version = target.rpartition('-')
|
||||||
|
name = PEP_503.sub("-", name).lower().replace('-', '_')
|
||||||
|
if name not in result_deps:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for result_version, meta_path in result_deps[name]:
|
||||||
|
if version != result_version:
|
||||||
|
shutil.rmtree(meta_path)
|
||||||
|
|
||||||
|
def upgrade(self, targets: List[str]) -> ExitStatus:
|
||||||
|
self.env.stdout.write(f"Upgrading {', '.join(targets)}...\n")
|
||||||
|
self.env.stdout.flush()
|
||||||
|
|
||||||
|
raw_stdout, exit_status = self._install(
|
||||||
|
targets,
|
||||||
|
mode='upgrade',
|
||||||
|
stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
if not raw_stdout:
|
||||||
|
return exit_status
|
||||||
|
|
||||||
|
stdout = raw_stdout.decode()
|
||||||
|
self.env.stdout.write(stdout)
|
||||||
|
|
||||||
|
installation_line = stdout.splitlines()[-1]
|
||||||
|
if installation_line.startswith('Successfully installed'):
|
||||||
|
self._clear_metadata(installation_line.split()[2:])
|
||||||
|
|
||||||
|
def _uninstall(self, target: str) -> Optional[ExitStatus]:
|
||||||
|
try:
|
||||||
|
distribution = importlib_metadata.distribution(target)
|
||||||
|
except importlib_metadata.PackageNotFoundError:
|
||||||
|
return self.fail('uninstall', target, 'package is not installed')
|
||||||
|
|
||||||
|
base_dir = Path(distribution.locate_file('.')).resolve()
|
||||||
|
if self.dir not in base_dir.parents:
|
||||||
|
# If the package is installed somewhere else (e.g on the site packages
|
||||||
|
# of the real python interpreter), than that means this package is not
|
||||||
|
# installed through us.
|
||||||
|
return self.fail('uninstall', target,
|
||||||
|
'package is not installed through httpie plugins'
|
||||||
|
' interface')
|
||||||
|
|
||||||
|
files = distribution.files
|
||||||
|
if files is None:
|
||||||
|
return self.fail('uninstall', target, 'couldn\'t locate the package')
|
||||||
|
|
||||||
|
# TODO: Consider handling failures here (e.g if it fails,
|
||||||
|
# just rever the operation and leave the site-packages
|
||||||
|
# in a proper shape).
|
||||||
|
for file in files:
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
os.unlink(distribution.locate_file(file))
|
||||||
|
|
||||||
|
metadata_path = getattr(distribution, '_path', None)
|
||||||
|
if (
|
||||||
|
metadata_path
|
||||||
|
and metadata_path.exists()
|
||||||
|
and not any(metadata_path.iterdir())
|
||||||
|
):
|
||||||
|
metadata_path.rmdir()
|
||||||
|
|
||||||
|
self.env.stdout.write(f'Successfully uninstalled {target}\n')
|
||||||
|
|
||||||
|
def uninstall(self, targets: List[str]) -> ExitStatus:
|
||||||
|
# Unfortunately uninstall doesn't work with custom pip schemes. See:
|
||||||
|
# - https://github.com/pypa/pip/issues/5595
|
||||||
|
# - https://github.com/pypa/pip/issues/4575
|
||||||
|
# so we have to implement our own uninstalling logic. Which works
|
||||||
|
# on top of the importlib_metadata.
|
||||||
|
|
||||||
|
exit_code = ExitStatus.SUCCESS
|
||||||
|
for target in targets:
|
||||||
|
exit_code |= self._uninstall(target) or ExitStatus.SUCCESS
|
||||||
|
return ExitStatus(exit_code)
|
||||||
|
|
||||||
|
def list(self) -> None:
|
||||||
|
from httpie.plugins.registry import plugin_manager
|
||||||
|
|
||||||
|
known_plugins = defaultdict(list)
|
||||||
|
|
||||||
|
for entry_point in plugin_manager.iter_entry_points(self.dir):
|
||||||
|
ep_info = (entry_point.group, entry_point.name)
|
||||||
|
ep_name = get_dist_name(entry_point) or entry_point.module
|
||||||
|
known_plugins[ep_name].append(ep_info)
|
||||||
|
|
||||||
|
for plugin, entry_points in known_plugins.items():
|
||||||
|
self.env.stdout.write(plugin)
|
||||||
|
|
||||||
|
version = importlib_metadata.version(plugin)
|
||||||
|
if version is not None:
|
||||||
|
self.env.stdout.write(f' ({version})')
|
||||||
|
self.env.stdout.write('\n')
|
||||||
|
|
||||||
|
for group, entry_point in sorted(entry_points):
|
||||||
|
self.env.stdout.write(f' {entry_point} ({group})\n')
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
action: Optional[str],
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> ExitStatus:
|
||||||
|
from httpie.plugins.manager import enable_plugins
|
||||||
|
|
||||||
|
if action is None:
|
||||||
|
parser.error(missing_subcommand('plugins'))
|
||||||
|
|
||||||
|
with enable_plugins(self.dir):
|
||||||
|
if action == 'install':
|
||||||
|
status = self.install(args.targets)
|
||||||
|
elif action == 'upgrade':
|
||||||
|
status = self.upgrade(args.targets)
|
||||||
|
elif action == 'uninstall':
|
||||||
|
status = self.uninstall(args.targets)
|
||||||
|
elif action == 'list':
|
||||||
|
status = self.list()
|
||||||
|
|
||||||
|
return status or ExitStatus.SUCCESS
|
@ -1,8 +1,18 @@
|
|||||||
from typing import Iterable
|
import requests
|
||||||
|
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Iterable, Union, NamedTuple
|
||||||
from urllib.parse import urlsplit
|
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 .compat import cached_property
|
||||||
|
from .utils import split_cookies, parse_content_type_header
|
||||||
|
|
||||||
|
|
||||||
class HTTPMessage:
|
class HTTPMessage:
|
||||||
@ -24,6 +34,11 @@ class HTTPMessage:
|
|||||||
"""Return a `str` with the message's headers."""
|
"""Return a `str` with the message's headers."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self) -> str:
|
||||||
|
"""Return metadata about the current message."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def encoding(self) -> str:
|
def encoding(self) -> str:
|
||||||
ct, params = parse_content_type_header(self.content_type)
|
ct, params = parse_content_type_header(self.content_type)
|
||||||
@ -72,10 +87,21 @@ class HTTPResponse(HTTPMessage):
|
|||||||
)
|
)
|
||||||
headers.extend(
|
headers.extend(
|
||||||
f'Set-Cookie: {cookie}'
|
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)
|
return '\r\n'.join(headers)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metadata(self) -> str:
|
||||||
|
data = {}
|
||||||
|
data['Elapsed time'] = str(self._orig.elapsed.total_seconds()) + 's'
|
||||||
|
return '\n'.join(
|
||||||
|
f'{key}: {value}'
|
||||||
|
for key, value in data.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HTTPRequest(HTTPMessage):
|
class HTTPRequest(HTTPMessage):
|
||||||
"""A :class:`requests.models.Request` wrapper."""
|
"""A :class:`requests.models.Request` wrapper."""
|
||||||
@ -96,7 +122,7 @@ class HTTPRequest(HTTPMessage):
|
|||||||
query=f'?{url.query}' if url.query else ''
|
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:
|
if 'Host' not in self._orig.headers:
|
||||||
headers['Host'] = url.netloc.split('@')[-1]
|
headers['Host'] = url.netloc.split('@')[-1]
|
||||||
|
|
||||||
@ -116,3 +142,67 @@ class HTTPRequest(HTTPMessage):
|
|||||||
# Happens with JSON/form request data parsed from the command line.
|
# Happens with JSON/form request data parsed from the command line.
|
||||||
body = body.encode()
|
body = body.encode()
|
||||||
return body or b''
|
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
|
import json
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type, Tuple
|
||||||
|
|
||||||
|
import pygments.formatters
|
||||||
import pygments.lexer
|
import pygments.lexer
|
||||||
import pygments.lexers
|
import pygments.lexers
|
||||||
import pygments.style
|
import pygments.style
|
||||||
@ -15,7 +16,8 @@ from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
|||||||
from pygments.util import ClassNotFound
|
from pygments.util import ClassNotFound
|
||||||
|
|
||||||
from ..lexers.json import EnhancedJsonLexer
|
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 ...context import Environment
|
||||||
from ...plugins import FormatterPlugin
|
from ...plugins import FormatterPlugin
|
||||||
|
|
||||||
@ -23,14 +25,15 @@ from ...plugins import FormatterPlugin
|
|||||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||||
DEFAULT_STYLE = AUTO_STYLE
|
DEFAULT_STYLE = AUTO_STYLE
|
||||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
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())
|
BUNDLED_STYLES = {
|
||||||
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
|
SOLARIZED_STYLE,
|
||||||
AVAILABLE_STYLES.add(AUTO_STYLE)
|
AUTO_STYLE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_styles():
|
||||||
|
return BUNDLED_STYLES | set(pygments.styles.get_all_styles())
|
||||||
|
|
||||||
|
|
||||||
class ColorFormatter(FormatterPlugin):
|
class ColorFormatter(FormatterPlugin):
|
||||||
@ -42,6 +45,7 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
group_name = 'colors'
|
group_name = 'colors'
|
||||||
|
metadata_lexer = MetadataLexer()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -60,23 +64,24 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
has_256_colors = env.colors == 256
|
has_256_colors = env.colors == 256
|
||||||
if use_auto_style or not has_256_colors:
|
if use_auto_style or not has_256_colors:
|
||||||
http_lexer = PygmentsHttpLexer()
|
http_lexer = PygmentsHttpLexer()
|
||||||
formatter = TerminalFormatter()
|
body_formatter = header_formatter = TerminalFormatter()
|
||||||
|
precise = False
|
||||||
else:
|
else:
|
||||||
from ..lexers.http import SimplifiedHTTPLexer
|
from ..lexers.http import SimplifiedHTTPLexer
|
||||||
http_lexer = SimplifiedHTTPLexer()
|
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
|
||||||
formatter = Terminal256Formatter(
|
http_lexer = SimplifiedHTTPLexer(precise=precise)
|
||||||
style=self.get_style_class(color_scheme)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.explicit_json = explicit_json # --json
|
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.http_lexer = http_lexer
|
||||||
|
self.metadata_lexer = MetadataLexer(precise=precise)
|
||||||
|
|
||||||
def format_headers(self, headers: str) -> str:
|
def format_headers(self, headers: str) -> str:
|
||||||
return pygments.highlight(
|
return pygments.highlight(
|
||||||
code=headers,
|
code=headers,
|
||||||
lexer=self.http_lexer,
|
lexer=self.http_lexer,
|
||||||
formatter=self.formatter,
|
formatter=self.header_formatter,
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
def format_body(self, body: str, mime: str) -> str:
|
def format_body(self, body: str, mime: str) -> str:
|
||||||
@ -85,10 +90,17 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
body = pygments.highlight(
|
body = pygments.highlight(
|
||||||
code=body,
|
code=body,
|
||||||
lexer=lexer,
|
lexer=lexer,
|
||||||
formatter=self.formatter,
|
formatter=self.body_formatter,
|
||||||
)
|
)
|
||||||
return body
|
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(
|
def get_lexer_for_body(
|
||||||
self, mime: str,
|
self, mime: str,
|
||||||
body: str
|
body: str
|
||||||
@ -99,6 +111,25 @@ class ColorFormatter(FormatterPlugin):
|
|||||||
body=body,
|
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
|
@staticmethod
|
||||||
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
|
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
|
||||||
try:
|
try:
|
||||||
@ -232,3 +263,124 @@ class Solarized256Style(pygments.style.Style):
|
|||||||
pygments.token.Token: BASE1,
|
pygments.token.Token: BASE1,
|
||||||
pygments.token.Token.Other: ORANGE,
|
pygments.token.Token.Other: ORANGE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PIE_HEADER_STYLE = {
|
||||||
|
# HTTP line / Headers / Etc.
|
||||||
|
pygments.token.Name.Namespace: 'bold primary',
|
||||||
|
pygments.token.Keyword.Reserved: 'bold grey',
|
||||||
|
pygments.token.Operator: 'bold grey',
|
||||||
|
pygments.token.Number: 'bold grey',
|
||||||
|
pygments.token.Name.Function.Magic: 'bold green',
|
||||||
|
pygments.token.Name.Exception: 'bold green',
|
||||||
|
pygments.token.Name.Attribute: 'blue',
|
||||||
|
pygments.token.String: 'primary',
|
||||||
|
|
||||||
|
# HTTP Methods
|
||||||
|
pygments.token.Name.Function: 'bold grey',
|
||||||
|
pygments.token.Name.Function.HTTP.GET: 'bold green',
|
||||||
|
pygments.token.Name.Function.HTTP.HEAD: 'bold green',
|
||||||
|
pygments.token.Name.Function.HTTP.POST: 'bold yellow',
|
||||||
|
pygments.token.Name.Function.HTTP.PUT: 'bold orange',
|
||||||
|
pygments.token.Name.Function.HTTP.PATCH: 'bold orange',
|
||||||
|
pygments.token.Name.Function.HTTP.DELETE: 'bold red',
|
||||||
|
|
||||||
|
# HTTP status codes
|
||||||
|
pygments.token.Number.HTTP.INFO: 'bold aqua',
|
||||||
|
pygments.token.Number.HTTP.OK: 'bold green',
|
||||||
|
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
|
||||||
|
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
|
||||||
|
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
pygments.token.Name.Decorator: 'grey',
|
||||||
|
pygments.token.Number.SPEED.FAST: 'bold green',
|
||||||
|
pygments.token.Number.SPEED.AVG: 'bold yellow',
|
||||||
|
pygments.token.Number.SPEED.SLOW: 'bold orange',
|
||||||
|
pygments.token.Number.SPEED.VERY_SLOW: 'bold red',
|
||||||
|
}
|
||||||
|
|
||||||
|
PIE_BODY_STYLE = {
|
||||||
|
# {}[]:
|
||||||
|
pygments.token.Punctuation: 'grey',
|
||||||
|
|
||||||
|
# Keys
|
||||||
|
pygments.token.Name.Tag: 'pink',
|
||||||
|
|
||||||
|
# Values
|
||||||
|
pygments.token.Literal.String: 'green',
|
||||||
|
pygments.token.Literal.String.Double: 'green',
|
||||||
|
pygments.token.Literal.Number: 'aqua',
|
||||||
|
pygments.token.Keyword: 'orange',
|
||||||
|
|
||||||
|
# Other stuff
|
||||||
|
pygments.token.Text: 'primary',
|
||||||
|
pygments.token.Name.Attribute: 'primary',
|
||||||
|
pygments.token.Name.Builtin: 'blue',
|
||||||
|
pygments.token.Name.Builtin.Pseudo: 'blue',
|
||||||
|
pygments.token.Name.Class: 'blue',
|
||||||
|
pygments.token.Name.Constant: 'orange',
|
||||||
|
pygments.token.Name.Decorator: 'blue',
|
||||||
|
pygments.token.Name.Entity: 'orange',
|
||||||
|
pygments.token.Name.Exception: 'yellow',
|
||||||
|
pygments.token.Name.Function: 'blue',
|
||||||
|
pygments.token.Name.Variable: 'blue',
|
||||||
|
pygments.token.String: 'aqua',
|
||||||
|
pygments.token.String.Backtick: 'secondary',
|
||||||
|
pygments.token.String.Char: 'aqua',
|
||||||
|
pygments.token.String.Doc: 'aqua',
|
||||||
|
pygments.token.String.Escape: 'red',
|
||||||
|
pygments.token.String.Heredoc: 'aqua',
|
||||||
|
pygments.token.String.Regex: 'red',
|
||||||
|
pygments.token.Number: 'aqua',
|
||||||
|
pygments.token.Operator: 'primary',
|
||||||
|
pygments.token.Operator.Word: 'green',
|
||||||
|
pygments.token.Comment: 'secondary',
|
||||||
|
pygments.token.Comment.Preproc: 'green',
|
||||||
|
pygments.token.Comment.Special: 'green',
|
||||||
|
pygments.token.Generic.Deleted: 'aqua',
|
||||||
|
pygments.token.Generic.Emph: 'italic',
|
||||||
|
pygments.token.Generic.Error: 'red',
|
||||||
|
pygments.token.Generic.Heading: 'orange',
|
||||||
|
pygments.token.Generic.Inserted: 'green',
|
||||||
|
pygments.token.Generic.Strong: 'bold',
|
||||||
|
pygments.token.Generic.Subheading: 'orange',
|
||||||
|
pygments.token.Token: 'primary',
|
||||||
|
pygments.token.Token.Other: 'orange',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_style(name, raw_styles, shade):
|
||||||
|
def format_value(value):
|
||||||
|
return ' '.join(
|
||||||
|
get_color(part, shade) or part
|
||||||
|
for part in value.split()
|
||||||
|
)
|
||||||
|
|
||||||
|
bases = (pygments.style.Style,)
|
||||||
|
data = {
|
||||||
|
'styles': {
|
||||||
|
key: format_value(value)
|
||||||
|
for key, value in raw_styles.items()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type(name, bases, data)
|
||||||
|
|
||||||
|
|
||||||
|
def make_styles():
|
||||||
|
styles = {}
|
||||||
|
|
||||||
|
for shade, name in SHADE_NAMES.items():
|
||||||
|
styles[name] = [
|
||||||
|
make_style(name, style_map, shade)
|
||||||
|
for style_name, style_map in [
|
||||||
|
(f'Pie{name}HeaderStyle', PIE_HEADER_STYLE),
|
||||||
|
(f'Pie{name}BodyStyle', PIE_BODY_STYLE),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
return styles
|
||||||
|
|
||||||
|
|
||||||
|
PIE_STYLES = make_styles()
|
||||||
|
BUNDLED_STYLES |= PIE_STYLES.keys()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import sys
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from ...encoding import UTF8
|
from ...encoding import UTF8
|
||||||
@ -8,27 +7,47 @@ if TYPE_CHECKING:
|
|||||||
from xml.dom.minidom import Document
|
from xml.dom.minidom import Document
|
||||||
|
|
||||||
|
|
||||||
|
XML_DECLARATION_OPEN = '<?xml'
|
||||||
|
XML_DECLARATION_CLOSE = '?>'
|
||||||
|
|
||||||
|
|
||||||
def parse_xml(data: str) -> 'Document':
|
def parse_xml(data: str) -> 'Document':
|
||||||
"""Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object."""
|
"""Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object."""
|
||||||
from defusedxml.minidom import parseString
|
from defusedxml.minidom import parseString
|
||||||
return parseString(data)
|
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',
|
def pretty_xml(document: 'Document',
|
||||||
|
declaration: Optional[str] = None,
|
||||||
encoding: Optional[str] = UTF8,
|
encoding: Optional[str] = UTF8,
|
||||||
indent: int = 2,
|
indent: int = 2) -> str:
|
||||||
standalone: Optional[bool] = None) -> str:
|
|
||||||
"""Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string."""
|
"""Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string."""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'encoding': encoding or UTF8,
|
'encoding': encoding or UTF8,
|
||||||
'indent': ' ' * indent,
|
'indent': ' ' * indent,
|
||||||
}
|
}
|
||||||
if standalone is not None and sys.version_info >= (3, 9):
|
|
||||||
kwargs['standalone'] = standalone
|
|
||||||
body = document.toprettyxml(**kwargs).decode(kwargs['encoding'])
|
body = document.toprettyxml(**kwargs).decode(kwargs['encoding'])
|
||||||
|
|
||||||
# Remove blank lines automatically added by `toprettyxml()`.
|
# 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):
|
class XMLFormatter(FormatterPlugin):
|
||||||
@ -44,6 +63,7 @@ class XMLFormatter(FormatterPlugin):
|
|||||||
from xml.parsers.expat import ExpatError
|
from xml.parsers.expat import ExpatError
|
||||||
from defusedxml.common import DefusedXmlException
|
from defusedxml.common import DefusedXmlException
|
||||||
|
|
||||||
|
declaration = parse_declaration(body)
|
||||||
try:
|
try:
|
||||||
parsed_body = parse_xml(body)
|
parsed_body = parse_xml(body)
|
||||||
except ExpatError:
|
except ExpatError:
|
||||||
@ -54,6 +74,6 @@ class XMLFormatter(FormatterPlugin):
|
|||||||
body = pretty_xml(parsed_body,
|
body = pretty_xml(parsed_body,
|
||||||
encoding=parsed_body.encoding,
|
encoding=parsed_body.encoding,
|
||||||
indent=self.format_options['xml']['indent'],
|
indent=self.format_options['xml']['indent'],
|
||||||
standalone=parsed_body.standalone)
|
declaration=declaration)
|
||||||
|
|
||||||
return body
|
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
|
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):
|
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||||
@ -18,7 +68,7 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
|||||||
# Request-Line
|
# Request-Line
|
||||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||||
pygments.lexer.bygroups(
|
pygments.lexer.bygroups(
|
||||||
pygments.token.Name.Function,
|
request_method,
|
||||||
pygments.token.Text,
|
pygments.token.Text,
|
||||||
pygments.token.Name.Namespace,
|
pygments.token.Name.Namespace,
|
||||||
pygments.token.Text,
|
pygments.token.Text,
|
||||||
@ -27,15 +77,13 @@ class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
|||||||
pygments.token.Number
|
pygments.token.Number
|
||||||
)),
|
)),
|
||||||
# Response Status-Line
|
# Response Status-Line
|
||||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
(r'(HTTP)(/)(\d+\.\d+)( +)(.+)',
|
||||||
pygments.lexer.bygroups(
|
pygments.lexer.bygroups(
|
||||||
pygments.token.Keyword.Reserved, # 'HTTP'
|
pygments.token.Keyword.Reserved, # 'HTTP'
|
||||||
pygments.token.Operator, # '/'
|
pygments.token.Operator, # '/'
|
||||||
pygments.token.Number, # Version
|
pygments.token.Number, # Version
|
||||||
pygments.token.Text,
|
pygments.token.Text,
|
||||||
pygments.token.Number, # Status code
|
http_response_type, # Status code and Reason
|
||||||
pygments.token.Text,
|
|
||||||
pygments.token.Name.Exception, # Reason
|
|
||||||
)),
|
)),
|
||||||
# Header
|
# Header
|
||||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||||
|
@ -20,7 +20,7 @@ class EnhancedJsonLexer(RegexLexer):
|
|||||||
tokens = {
|
tokens = {
|
||||||
'root': [
|
'root': [
|
||||||
# Eventual non-JSON data prefix followed by actual JSON body.
|
# Eventual non-JSON data prefix followed by actual JSON body.
|
||||||
# FIX: data prefix + number (integer or float) are not correctly handled.
|
# FIX: data prefix + number (integer or float) is not correctly handled.
|
||||||
(
|
(
|
||||||
fr'({PREFIX_REGEX})' + r'((?:[{\["]|true|false|null).+)',
|
fr'({PREFIX_REGEX})' + r'((?:[{\["]|true|false|null).+)',
|
||||||
bygroups(PREFIX_TOKEN, using(JsonLexer))
|
bygroups(PREFIX_TOKEN, using(JsonLexer))
|
||||||
|
57
httpie/output/lexers/metadata.py
Normal file
57
httpie/output/lexers/metadata.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import pygments
|
||||||
|
from httpie.output.lexers.common import precise
|
||||||
|
|
||||||
|
SPEED_TOKENS = {
|
||||||
|
0.45: pygments.token.Number.SPEED.FAST,
|
||||||
|
1.00: pygments.token.Number.SPEED.AVG,
|
||||||
|
2.50: pygments.token.Number.SPEED.SLOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def speed_based_token(lexer, match, ctx):
|
||||||
|
try:
|
||||||
|
value = float(match.group())
|
||||||
|
except ValueError:
|
||||||
|
return pygments.token.Number
|
||||||
|
|
||||||
|
for limit, token in SPEED_TOKENS.items():
|
||||||
|
if value <= limit:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
token = pygments.token.Number.SPEED.VERY_SLOW
|
||||||
|
|
||||||
|
response_type = precise(
|
||||||
|
lexer,
|
||||||
|
token,
|
||||||
|
pygments.token.Number
|
||||||
|
)
|
||||||
|
yield match.start(), response_type, match.group()
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataLexer(pygments.lexer.RegexLexer):
|
||||||
|
"""Simple HTTPie metadata lexer."""
|
||||||
|
|
||||||
|
tokens = {
|
||||||
|
'root': [
|
||||||
|
(
|
||||||
|
r'(Elapsed time)( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
|
||||||
|
pygments.token.Name.Decorator, # Name
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Operator, # Colon
|
||||||
|
pygments.token.Text,
|
||||||
|
speed_based_token,
|
||||||
|
pygments.token.Name.Builtin # Value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# Generic item
|
||||||
|
(
|
||||||
|
r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||||
|
pygments.token.Name.Decorator, # Name
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Operator, # Colon
|
||||||
|
pygments.token.Text,
|
||||||
|
pygments.token.Text # Value
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
@ -51,3 +51,8 @@ class Formatting:
|
|||||||
for p in self.enabled_plugins:
|
for p in self.enabled_plugins:
|
||||||
content = p.format_body(content, mime)
|
content = p.format_body(content, mime)
|
||||||
return content
|
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 abc import ABCMeta, abstractmethod
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import Callable, Iterable, Union
|
from typing import Callable, Iterable, Optional, Union
|
||||||
|
|
||||||
from .processing import Conversion, Formatting
|
from .processing import Conversion, Formatting
|
||||||
from ..context import Environment
|
from ..context import Environment
|
||||||
from ..encoding import smart_decode, smart_encode, UTF8
|
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 = (
|
BINARY_SUPPRESSED_NOTICE = (
|
||||||
@ -32,47 +33,55 @@ class BaseStream(metaclass=ABCMeta):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
msg: HTTPMessage,
|
msg: HTTPMessage,
|
||||||
with_headers=True,
|
output_options: OutputOptions,
|
||||||
with_body=True,
|
|
||||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param msg: a :class:`models.HTTPMessage` subclass
|
:param msg: a :class:`models.HTTPMessage` subclass
|
||||||
:param with_headers: if `True`, headers will be included
|
:param output_options: a :class:`OutputOptions` instance to represent
|
||||||
:param with_body: if `True`, body will be included
|
which parts of the message is printed.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert with_headers or with_body
|
assert output_options.any()
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.with_headers = with_headers
|
self.output_options = output_options
|
||||||
self.with_body = with_body
|
|
||||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||||
|
|
||||||
def get_headers(self) -> bytes:
|
def get_headers(self) -> bytes:
|
||||||
"""Return the headers' bytes."""
|
"""Return the headers' bytes."""
|
||||||
return self.msg.headers.encode()
|
return self.msg.headers.encode()
|
||||||
|
|
||||||
|
def get_metadata(self) -> bytes:
|
||||||
|
"""Return the message metadata."""
|
||||||
|
return self.msg.metadata.encode()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def iter_body(self) -> Iterable[bytes]:
|
def iter_body(self) -> Iterable[bytes]:
|
||||||
"""Return an iterator over the message body."""
|
"""Return an iterator over the message body."""
|
||||||
|
|
||||||
def __iter__(self) -> Iterable[bytes]:
|
def __iter__(self) -> Iterable[bytes]:
|
||||||
"""Return an iterator over `self.msg`."""
|
"""Return an iterator over `self.msg`."""
|
||||||
if self.with_headers:
|
if self.output_options.headers:
|
||||||
yield self.get_headers()
|
yield self.get_headers()
|
||||||
yield b'\r\n\r\n'
|
yield b'\r\n\r\n'
|
||||||
|
|
||||||
if self.with_body:
|
if self.output_options.body:
|
||||||
try:
|
try:
|
||||||
for chunk in self.iter_body():
|
for chunk in self.iter_body():
|
||||||
yield chunk
|
yield chunk
|
||||||
if self.on_body_chunk_downloaded:
|
if self.on_body_chunk_downloaded:
|
||||||
self.on_body_chunk_downloaded(chunk)
|
self.on_body_chunk_downloaded(chunk)
|
||||||
except DataSuppressedError as e:
|
except DataSuppressedError as e:
|
||||||
if self.with_headers:
|
if self.output_options.headers:
|
||||||
yield b'\n'
|
yield b'\n'
|
||||||
yield e.message
|
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):
|
class RawStream(BaseStream):
|
||||||
"""The message is streamed in chunks with no processing."""
|
"""The message is streamed in chunks with no processing."""
|
||||||
@ -88,6 +97,9 @@ class RawStream(BaseStream):
|
|||||||
return self.msg.iter_body(self.chunk_size)
|
return self.msg.iter_body(self.chunk_size)
|
||||||
|
|
||||||
|
|
||||||
|
ENCODING_GUESS_THRESHOLD = 3
|
||||||
|
|
||||||
|
|
||||||
class EncodedStream(BaseStream):
|
class EncodedStream(BaseStream):
|
||||||
"""Encoded HTTP message stream.
|
"""Encoded HTTP message stream.
|
||||||
|
|
||||||
@ -106,8 +118,12 @@ class EncodedStream(BaseStream):
|
|||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.mime = mime_overwrite or self.msg.content_type
|
if mime_overwrite:
|
||||||
self.encoding = encoding_overwrite or self.msg.encoding
|
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:
|
if env.stdout_isatty:
|
||||||
# Use the encoding supported by the terminal.
|
# Use the encoding supported by the terminal.
|
||||||
output_encoding = env.stdout_encoding
|
output_encoding = env.stdout_encoding
|
||||||
@ -121,9 +137,33 @@ class EncodedStream(BaseStream):
|
|||||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||||
if b'\0' in line:
|
if b'\0' in line:
|
||||||
raise BinarySuppressedError()
|
raise BinarySuppressedError()
|
||||||
line = smart_decode(line, self.encoding)
|
line = self.decode_chunk(line)
|
||||||
yield smart_encode(line, self.output_encoding) + lf
|
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):
|
class PrettyStream(EncodedStream):
|
||||||
"""In addition to :class:`EncodedStream` behaviour, this stream applies
|
"""In addition to :class:`EncodedStream` behaviour, this stream applies
|
||||||
@ -149,6 +189,10 @@ class PrettyStream(EncodedStream):
|
|||||||
return self.formatting.format_headers(
|
return self.formatting.format_headers(
|
||||||
self.msg.headers).encode(self.output_encoding)
|
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]:
|
def iter_body(self) -> Iterable[bytes]:
|
||||||
first_chunk = True
|
first_chunk = True
|
||||||
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
||||||
@ -174,7 +218,7 @@ class PrettyStream(EncodedStream):
|
|||||||
if not isinstance(chunk, str):
|
if not isinstance(chunk, str):
|
||||||
# Text when a converter has been used,
|
# Text when a converter has been used,
|
||||||
# otherwise it will always be bytes.
|
# 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)
|
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
|
||||||
return smart_encode(chunk, self.output_encoding)
|
return smart_encode(chunk, self.output_encoding)
|
||||||
|
|
||||||
|
0
httpie/output/ui/__init__.py
Normal file
0
httpie/output/ui/__init__.py
Normal file
161
httpie/output/ui/palette.py
Normal file
161
httpie/output/ui/palette.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# Copy the brand palette
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
COLOR_PALETTE = {
|
||||||
|
'transparent': 'transparent',
|
||||||
|
'current': 'currentColor',
|
||||||
|
'white': '#F5F5F0',
|
||||||
|
'black': '#1C1818',
|
||||||
|
'grey': {
|
||||||
|
'50': '#F5F5F0',
|
||||||
|
'100': '#EDEDEB',
|
||||||
|
'200': '#D1D1CF',
|
||||||
|
'300': '#B5B5B2',
|
||||||
|
'400': '#999999',
|
||||||
|
'500': '#7D7D7D',
|
||||||
|
'600': '#666663',
|
||||||
|
'700': '#4F4D4D',
|
||||||
|
'800': '#363636',
|
||||||
|
'900': '#1C1818',
|
||||||
|
'DEFAULT': '#7D7D7D',
|
||||||
|
},
|
||||||
|
'aqua': {
|
||||||
|
'50': '#E8F0F5',
|
||||||
|
'100': '#D6E3ED',
|
||||||
|
'200': '#C4D9E5',
|
||||||
|
'300': '#B0CCDE',
|
||||||
|
'400': '#9EBFD6',
|
||||||
|
'500': '#8CB4CD',
|
||||||
|
'600': '#7A9EB5',
|
||||||
|
'700': '#698799',
|
||||||
|
'800': '#597082',
|
||||||
|
'900': '#455966',
|
||||||
|
'DEFAULT': '#8CB4CD',
|
||||||
|
},
|
||||||
|
'purple': {
|
||||||
|
'50': '#F0E0FC',
|
||||||
|
'100': '#E3C7FA',
|
||||||
|
'200': '#D9ADF7',
|
||||||
|
'300': '#CC96F5',
|
||||||
|
'400': '#BF7DF2',
|
||||||
|
'500': '#B464F0',
|
||||||
|
'600': '#9E54D6',
|
||||||
|
'700': '#8745BA',
|
||||||
|
'800': '#70389E',
|
||||||
|
'900': '#5C2982',
|
||||||
|
'DEFAULT': '#B464F0',
|
||||||
|
},
|
||||||
|
'orange': {
|
||||||
|
'50': '#FFEDDB',
|
||||||
|
'100': '#FFDEBF',
|
||||||
|
'200': '#FFCFA3',
|
||||||
|
'300': '#FFBF87',
|
||||||
|
'400': '#FFB06B',
|
||||||
|
'500': '#FFA24E',
|
||||||
|
'600': '#F2913D',
|
||||||
|
'700': '#E3822B',
|
||||||
|
'800': '#D6701C',
|
||||||
|
'900': '#C75E0A',
|
||||||
|
'DEFAULT': '#FFA24E',
|
||||||
|
},
|
||||||
|
'red': {
|
||||||
|
'50': '#FFE0DE',
|
||||||
|
'100': '#FFC7C4',
|
||||||
|
'200': '#FFB0AB',
|
||||||
|
'300': '#FF968F',
|
||||||
|
'400': '#FF8075',
|
||||||
|
'500': '#FF665B',
|
||||||
|
'600': '#E34F45',
|
||||||
|
'700': '#C7382E',
|
||||||
|
'800': '#AD2117',
|
||||||
|
'900': '#910A00',
|
||||||
|
'DEFAULT': '#FF665B',
|
||||||
|
},
|
||||||
|
'blue': {
|
||||||
|
'50': '#DBE3FA',
|
||||||
|
'100': '#BFCFF5',
|
||||||
|
'200': '#A1B8F2',
|
||||||
|
'300': '#85A3ED',
|
||||||
|
'400': '#698FEB',
|
||||||
|
'500': '#4B78E6',
|
||||||
|
'600': '#426BD1',
|
||||||
|
'700': '#3B5EBA',
|
||||||
|
'800': '#3354A6',
|
||||||
|
'900': '#2B478F',
|
||||||
|
'DEFAULT': '#4B78E6',
|
||||||
|
},
|
||||||
|
'pink': {
|
||||||
|
'50': '#FFEBFF',
|
||||||
|
'100': '#FCDBFC',
|
||||||
|
'200': '#FCCCFC',
|
||||||
|
'300': '#FCBAFC',
|
||||||
|
'400': '#FAABFA',
|
||||||
|
'500': '#FA9BFA',
|
||||||
|
'600': '#DE85DE',
|
||||||
|
'700': '#C26EC2',
|
||||||
|
'800': '#A854A6',
|
||||||
|
'900': '#8C3D8A',
|
||||||
|
'DEFAULT': '#FA9BFA',
|
||||||
|
},
|
||||||
|
'green': {
|
||||||
|
'50': '#E3F7E8',
|
||||||
|
'100': '#CCF2D6',
|
||||||
|
'200': '#B5EDC4',
|
||||||
|
'300': '#A1E8B0',
|
||||||
|
'400': '#8AE09E',
|
||||||
|
'500': '#73DC8C',
|
||||||
|
'600': '#63C27A',
|
||||||
|
'700': '#52AB66',
|
||||||
|
'800': '#429154',
|
||||||
|
'900': '#307842',
|
||||||
|
'DEFAULT': '#73DC8C',
|
||||||
|
},
|
||||||
|
'yellow': {
|
||||||
|
'50': '#F7F7DB',
|
||||||
|
'100': '#F2F2BF',
|
||||||
|
'200': '#EDEDA6',
|
||||||
|
'300': '#E5E88A',
|
||||||
|
'400': '#E0E36E',
|
||||||
|
'500': '#DBDE52',
|
||||||
|
'600': '#CCCC3D',
|
||||||
|
'700': '#BABA29',
|
||||||
|
'800': '#ABA614',
|
||||||
|
'900': '#999400',
|
||||||
|
'DEFAULT': '#DBDE52',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grey is the same no matter shade for the colors
|
||||||
|
COLOR_PALETTE['grey'] = {
|
||||||
|
shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys()
|
||||||
|
}
|
||||||
|
|
||||||
|
COLOR_PALETTE['primary'] = {
|
||||||
|
'700': COLOR_PALETTE['black'],
|
||||||
|
'600': 'ansibrightblack',
|
||||||
|
'500': COLOR_PALETTE['white'],
|
||||||
|
}
|
||||||
|
|
||||||
|
COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'}
|
||||||
|
|
||||||
|
SHADE_NAMES = {
|
||||||
|
'500': 'pie-dark',
|
||||||
|
'600': 'pie',
|
||||||
|
'700': 'pie-light'
|
||||||
|
}
|
||||||
|
|
||||||
|
SHADES = [
|
||||||
|
'50',
|
||||||
|
*map(str, range(100, 1000, 100))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_color(color: str, shade: str) -> Optional[str]:
|
||||||
|
if color not in COLOR_PALETTE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
color_code = COLOR_PALETTE[color]
|
||||||
|
if isinstance(color_code, dict) and shade in color_code:
|
||||||
|
return color_code[shade]
|
||||||
|
else:
|
||||||
|
return color_code
|
@ -2,10 +2,16 @@ import argparse
|
|||||||
import errno
|
import errno
|
||||||
from typing import IO, TextIO, Tuple, Type, Union
|
from typing import IO, TextIO, Tuple, Type, Union
|
||||||
|
|
||||||
import requests
|
from ..cli.dicts import HTTPHeadersDict
|
||||||
|
|
||||||
from ..context import Environment
|
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 .processing import Conversion, Formatting
|
||||||
from .streams import (
|
from .streams import (
|
||||||
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
||||||
@ -17,21 +23,19 @@ MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
|
|||||||
|
|
||||||
|
|
||||||
def write_message(
|
def write_message(
|
||||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
requests_message: RequestsMessage,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
with_headers=False,
|
output_options: OutputOptions,
|
||||||
with_body=False,
|
|
||||||
):
|
):
|
||||||
if not (with_body or with_headers):
|
if not output_options.any():
|
||||||
return
|
return
|
||||||
write_stream_kwargs = {
|
write_stream_kwargs = {
|
||||||
'stream': build_output_stream_for_message(
|
'stream': build_output_stream_for_message(
|
||||||
args=args,
|
args=args,
|
||||||
env=env,
|
env=env,
|
||||||
requests_message=requests_message,
|
requests_message=requests_message,
|
||||||
with_body=with_body,
|
output_options=output_options,
|
||||||
with_headers=with_headers,
|
|
||||||
),
|
),
|
||||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||||
'outfile': env.stdout,
|
'outfile': env.stdout,
|
||||||
@ -93,26 +97,25 @@ def write_stream_with_colors_win(
|
|||||||
def build_output_stream_for_message(
|
def build_output_stream_for_message(
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
env: Environment,
|
env: Environment,
|
||||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
requests_message: RequestsMessage,
|
||||||
with_headers: bool,
|
output_options: OutputOptions,
|
||||||
with_body: bool,
|
|
||||||
):
|
):
|
||||||
message_type = {
|
message_type = {
|
||||||
requests.PreparedRequest: HTTPRequest,
|
RequestsMessageKind.REQUEST: HTTPRequest,
|
||||||
requests.Response: HTTPResponse,
|
RequestsMessageKind.RESPONSE: HTTPResponse,
|
||||||
}[type(requests_message)]
|
}[output_options.kind]
|
||||||
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
||||||
env=env,
|
env=env,
|
||||||
args=args,
|
args=args,
|
||||||
message_type=message_type,
|
message_type=message_type,
|
||||||
|
headers=requests_message.headers
|
||||||
)
|
)
|
||||||
yield from stream_class(
|
yield from stream_class(
|
||||||
msg=message_type(requests_message),
|
msg=message_type(requests_message),
|
||||||
with_headers=with_headers,
|
output_options=output_options,
|
||||||
with_body=with_body,
|
|
||||||
**stream_kwargs,
|
**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)):
|
and not getattr(requests_message, 'is_body_upload_chunk', False)):
|
||||||
# Ensure a blank line after the response body.
|
# Ensure a blank line after the response body.
|
||||||
# For terminal output only.
|
# For terminal output only.
|
||||||
@ -123,16 +126,23 @@ def get_stream_type_and_kwargs(
|
|||||||
env: Environment,
|
env: Environment,
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
message_type: Type[HTTPMessage],
|
message_type: Type[HTTPMessage],
|
||||||
|
headers: HTTPHeadersDict,
|
||||||
) -> Tuple[Type['BaseStream'], dict]:
|
) -> Tuple[Type['BaseStream'], dict]:
|
||||||
"""Pick the right stream type and kwargs for it based on `env` and `args`.
|
"""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:
|
if not env.stdout_isatty and not args.prettify:
|
||||||
stream_class = RawStream
|
stream_class = RawStream
|
||||||
stream_kwargs = {
|
stream_kwargs = {
|
||||||
'chunk_size': (
|
'chunk_size': (
|
||||||
RawStream.CHUNK_SIZE_BY_LINE
|
RawStream.CHUNK_SIZE_BY_LINE
|
||||||
if args.stream
|
if is_stream
|
||||||
else RawStream.CHUNK_SIZE
|
else RawStream.CHUNK_SIZE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -147,7 +157,7 @@ def get_stream_type_and_kwargs(
|
|||||||
'encoding_overwrite': args.response_charset,
|
'encoding_overwrite': args.response_charset,
|
||||||
})
|
})
|
||||||
if args.prettify:
|
if args.prettify:
|
||||||
stream_class = PrettyStream if args.stream else BufferedPrettyStream
|
stream_class = PrettyStream if is_stream else BufferedPrettyStream
|
||||||
stream_kwargs.update({
|
stream_kwargs.update({
|
||||||
'conversion': Conversion(),
|
'conversion': Conversion(),
|
||||||
'formatting': Formatting(
|
'formatting': Formatting(
|
||||||
|
@ -155,3 +155,11 @@ class FormatterPlugin(BasePlugin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return content
|
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}'
|
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):
|
class BasicAuthPlugin(BuiltinAuthPlugin):
|
||||||
name = 'Basic HTTP auth'
|
name = 'Basic HTTP auth'
|
||||||
auth_type = 'basic'
|
auth_type = 'basic'
|
||||||
@ -56,3 +66,14 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
|
|||||||
password: str
|
password: str
|
||||||
) -> requests.auth.HTTPDigestAuth:
|
) -> requests.auth.HTTPDigestAuth:
|
||||||
return requests.auth.HTTPDigestAuth(username, password)
|
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 itertools import groupby
|
||||||
from operator import attrgetter
|
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 ..utils import repr_dict, as_site
|
||||||
from . import AuthPlugin, ConverterPlugin, FormatterPlugin
|
from . import AuthPlugin, ConverterPlugin, FormatterPlugin, TransportPlugin
|
||||||
from .base import BasePlugin, TransportPlugin
|
from .base import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
ENTRY_POINT_NAMES = [
|
ENTRY_POINT_CLASSES = {
|
||||||
'httpie.plugins.auth.v1',
|
'httpie.plugins.auth.v1': AuthPlugin,
|
||||||
'httpie.plugins.formatter.v1',
|
'httpie.plugins.converter.v1': ConverterPlugin,
|
||||||
'httpie.plugins.converter.v1',
|
'httpie.plugins.formatter.v1': FormatterPlugin,
|
||||||
'httpie.plugins.transport.v1',
|
'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):
|
class PluginManager(list):
|
||||||
|
|
||||||
def register(self, *plugins: Type[BasePlugin]):
|
def register(self, *plugins: Type[BasePlugin]):
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
self.append(plugin)
|
self.append(plugin)
|
||||||
@ -29,12 +52,28 @@ class PluginManager(list):
|
|||||||
def filter(self, by_type=Type[BasePlugin]):
|
def filter(self, by_type=Type[BasePlugin]):
|
||||||
return [plugin for plugin in self if issubclass(plugin, by_type)]
|
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):
|
||||||
for entry_point_name in ENTRY_POINT_NAMES:
|
with enable_plugins(directory):
|
||||||
for entry_point in iter_entry_points(entry_point_name):
|
eps = importlib_metadata.entry_points()
|
||||||
|
|
||||||
|
for entry_point_name in ENTRY_POINT_NAMES:
|
||||||
|
yield from find_entry_points(eps, group=entry_point_name)
|
||||||
|
|
||||||
|
def load_installed_plugins(self, directory: Optional[Path] = None):
|
||||||
|
for entry_point in self.iter_entry_points(directory):
|
||||||
|
plugin_name = get_dist_name(entry_point)
|
||||||
|
try:
|
||||||
plugin = entry_point.load()
|
plugin = entry_point.load()
|
||||||
plugin.package_name = entry_point.dist.key
|
except BaseException as exc:
|
||||||
self.register(entry_point.load())
|
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
|
# Auth
|
||||||
def get_auth_plugins(self) -> List[Type[AuthPlugin]]:
|
def get_auth_plugins(self) -> List[Type[AuthPlugin]]:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from .manager import PluginManager
|
from .manager import PluginManager
|
||||||
from .builtin import BasicAuthPlugin, DigestAuthPlugin
|
from .builtin import BasicAuthPlugin, DigestAuthPlugin, BearerAuthPlugin
|
||||||
from ..output.formatters.headers import HeadersFormatter
|
from ..output.formatters.headers import HeadersFormatter
|
||||||
from ..output.formatters.json import JSONFormatter
|
from ..output.formatters.json import JSONFormatter
|
||||||
from ..output.formatters.xml import XMLFormatter
|
from ..output.formatters.xml import XMLFormatter
|
||||||
@ -13,6 +13,7 @@ plugin_manager = PluginManager()
|
|||||||
plugin_manager.register(
|
plugin_manager.register(
|
||||||
BasicAuthPlugin,
|
BasicAuthPlugin,
|
||||||
DigestAuthPlugin,
|
DigestAuthPlugin,
|
||||||
|
BearerAuthPlugin,
|
||||||
HeadersFormatter,
|
HeadersFormatter,
|
||||||
JSONFormatter,
|
JSONFormatter,
|
||||||
XMLFormatter,
|
XMLFormatter,
|
||||||
|
@ -13,7 +13,7 @@ from urllib.parse import urlsplit
|
|||||||
from requests.auth import AuthBase
|
from requests.auth import AuthBase
|
||||||
from requests.cookies import RequestsCookieJar, create_cookie
|
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 .config import BaseConfigDict, DEFAULT_CONFIG_DIR
|
||||||
from .plugins.registry import plugin_manager
|
from .plugins.registry import plugin_manager
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class Session(BaseConfigDict):
|
|||||||
'password': None
|
'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
|
Update the session headers with the request ones while ignoring
|
||||||
certain name prefixes.
|
certain name prefixes.
|
||||||
@ -98,8 +98,8 @@ class Session(BaseConfigDict):
|
|||||||
self['headers'] = dict(headers)
|
self['headers'] = dict(headers)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def headers(self) -> RequestHeadersDict:
|
def headers(self) -> HTTPHeadersDict:
|
||||||
return RequestHeadersDict(self['headers'])
|
return HTTPHeadersDict(self['headers'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookies(self) -> RequestsCookieJar:
|
def cookies(self) -> RequestsCookieJar:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
from requests.adapters import HTTPAdapter
|
from httpie.adapters import HTTPAdapter
|
||||||
# noinspection PyPackageRequirements
|
# noinspection PyPackageRequirements
|
||||||
from urllib3.util.ssl_ import (
|
from urllib3.util.ssl_ import (
|
||||||
DEFAULT_CIPHERS, create_urllib3_context,
|
DEFAULT_CIPHERS, create_urllib3_context,
|
@ -1,15 +1,27 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
import zlib
|
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
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests.utils import super_len
|
from requests.utils import super_len
|
||||||
from requests_toolbelt import MultipartEncoder
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests_toolbelt import MultipartEncoder
|
||||||
|
|
||||||
|
from .context import Environment
|
||||||
from .cli.dicts import MultipartRequestDataDict, RequestDataDict
|
from .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):
|
def __init__(self, stream: Iterable, callback: Callable):
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
@ -20,10 +32,10 @@ class ChunkedUploadStream:
|
|||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
|
|
||||||
class ChunkedMultipartUploadStream:
|
class ChunkedMultipartUploadStream(ChunkedStream):
|
||||||
chunk_size = 100 * 1024
|
chunk_size = 100 * 1024
|
||||||
|
|
||||||
def __init__(self, encoder: MultipartEncoder):
|
def __init__(self, encoder: 'MultipartEncoder'):
|
||||||
self.encoder = encoder
|
self.encoder = encoder
|
||||||
|
|
||||||
def __iter__(self) -> Iterable[Union[str, bytes]]:
|
def __iter__(self) -> Iterable[Union[str, bytes]]:
|
||||||
@ -34,75 +46,157 @@ class ChunkedMultipartUploadStream:
|
|||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytes(data: Union[str, bytes]) -> bytes:
|
||||||
|
if isinstance(data, str):
|
||||||
|
return data.encode()
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
CallbackT = Callable[[bytes], bytes]
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_function_with_callback(
|
||||||
|
func: Callable[..., Any],
|
||||||
|
callback: CallbackT
|
||||||
|
) -> Callable[..., Any]:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
chunk = func(*args, **kwargs)
|
||||||
|
callback(chunk)
|
||||||
|
return chunk
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def is_stdin(file: IO) -> bool:
|
||||||
|
try:
|
||||||
|
file_no = file.fileno()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return file_no == sys.stdin.fileno()
|
||||||
|
|
||||||
|
|
||||||
|
READ_THRESHOLD = float(os.getenv("HTTPIE_STDIN_READ_WARN_THRESHOLD", 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
def observe_stdin_for_data_thread(env: Environment, file: IO) -> None:
|
||||||
|
# Windows unfortunately does not support select() operation
|
||||||
|
# on regular files, like stdin in our use case.
|
||||||
|
# https://docs.python.org/3/library/select.html#select.select
|
||||||
|
if is_windows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# If the user configures READ_THRESHOLD to be 0, then
|
||||||
|
# disable this warning.
|
||||||
|
if READ_THRESHOLD == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
import select
|
||||||
|
import threading
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
can_read, _, _ = select.select([file], [], [], READ_THRESHOLD)
|
||||||
|
if not can_read:
|
||||||
|
env.stderr.write(
|
||||||
|
f'> warning: no stdin data read in {READ_THRESHOLD}s '
|
||||||
|
f'(perhaps you want to --ignore-stdin)\n'
|
||||||
|
f'> See: https://httpie.io/docs/cli/best-practices\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=worker
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_file_for_upload(
|
||||||
|
env: Environment,
|
||||||
|
file: Union[IO, 'MultipartEncoder'],
|
||||||
|
callback: CallbackT,
|
||||||
|
chunked: bool = False,
|
||||||
|
content_length_header_value: Optional[int] = None,
|
||||||
|
) -> Union[bytes, IO, ChunkedStream]:
|
||||||
|
if not super_len(file):
|
||||||
|
if is_stdin(file):
|
||||||
|
observe_stdin_for_data_thread(env, file)
|
||||||
|
# Zero-length -> assume stdin.
|
||||||
|
if content_length_header_value is None and not chunked:
|
||||||
|
# Read the whole stdin to determine `Content-Length`.
|
||||||
|
#
|
||||||
|
# TODO: Instead of opt-in --chunked, consider making
|
||||||
|
# `Transfer-Encoding: chunked` for STDIN opt-out via
|
||||||
|
# something like --no-chunked.
|
||||||
|
# This would be backwards-incompatible so wait until v3.0.0.
|
||||||
|
#
|
||||||
|
file = as_bytes(file.read())
|
||||||
|
else:
|
||||||
|
file.read = _wrap_function_with_callback(
|
||||||
|
file.read,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if chunked:
|
||||||
|
from requests_toolbelt import MultipartEncoder
|
||||||
|
if isinstance(file, MultipartEncoder):
|
||||||
|
return ChunkedMultipartUploadStream(
|
||||||
|
encoder=file,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ChunkedUploadStream(
|
||||||
|
stream=file,
|
||||||
|
callback=callback,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return file
|
||||||
|
|
||||||
|
|
||||||
def prepare_request_body(
|
def prepare_request_body(
|
||||||
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
|
env: Environment,
|
||||||
body_read_callback: Callable[[bytes], bytes],
|
raw_body: Union[str, bytes, IO, 'MultipartEncoder', RequestDataDict],
|
||||||
content_length_header_value: int = None,
|
body_read_callback: CallbackT,
|
||||||
chunked=False,
|
offline: bool = False,
|
||||||
offline=False,
|
chunked: bool = False,
|
||||||
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
|
content_length_header_value: Optional[int] = None,
|
||||||
|
) -> Union[bytes, IO, 'MultipartEncoder', ChunkedStream]:
|
||||||
is_file_like = hasattr(body, 'read')
|
is_file_like = hasattr(raw_body, 'read')
|
||||||
|
if isinstance(raw_body, (bytes, str)):
|
||||||
if isinstance(body, RequestDataDict):
|
body = as_bytes(raw_body)
|
||||||
body = urlencode(body, doseq=True)
|
elif isinstance(raw_body, RequestDataDict):
|
||||||
|
body = as_bytes(urlencode(raw_body, doseq=True))
|
||||||
|
else:
|
||||||
|
body = raw_body
|
||||||
|
|
||||||
if offline:
|
if offline:
|
||||||
if is_file_like:
|
if is_file_like:
|
||||||
return body.read()
|
return as_bytes(raw_body.read())
|
||||||
return body
|
|
||||||
|
|
||||||
if not is_file_like:
|
|
||||||
if chunked:
|
|
||||||
body = ChunkedUploadStream(
|
|
||||||
# Pass the entire body as one chunk.
|
|
||||||
stream=(chunk.encode() for chunk in [body]),
|
|
||||||
callback=body_read_callback,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# File-like object.
|
|
||||||
|
|
||||||
if not super_len(body):
|
|
||||||
# Zero-length -> assume stdin.
|
|
||||||
if content_length_header_value is None and not chunked:
|
|
||||||
#
|
|
||||||
# Read the whole stdin to determine `Content-Length`.
|
|
||||||
#
|
|
||||||
# TODO: Instead of opt-in --chunked, consider making
|
|
||||||
# `Transfer-Encoding: chunked` for STDIN opt-out via
|
|
||||||
# something like --no-chunked.
|
|
||||||
# This would be backwards-incompatible so wait until v3.0.0.
|
|
||||||
#
|
|
||||||
body = body.read()
|
|
||||||
else:
|
else:
|
||||||
orig_read = body.read
|
return body
|
||||||
|
|
||||||
def new_read(*args):
|
if is_file_like:
|
||||||
chunk = orig_read(*args)
|
return _prepare_file_for_upload(
|
||||||
body_read_callback(chunk)
|
env,
|
||||||
return chunk
|
body,
|
||||||
|
chunked=chunked,
|
||||||
body.read = new_read
|
callback=body_read_callback,
|
||||||
|
content_length_header_value=content_length_header_value
|
||||||
if chunked:
|
)
|
||||||
if isinstance(body, MultipartEncoder):
|
elif chunked:
|
||||||
body = ChunkedMultipartUploadStream(
|
return ChunkedUploadStream(
|
||||||
encoder=body,
|
stream=iter([body]),
|
||||||
)
|
callback=body_read_callback
|
||||||
else:
|
)
|
||||||
body = ChunkedUploadStream(
|
else:
|
||||||
stream=body,
|
return body
|
||||||
callback=body_read_callback,
|
|
||||||
)
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
def get_multipart_data_and_content_type(
|
def get_multipart_data_and_content_type(
|
||||||
data: MultipartRequestDataDict,
|
data: MultipartRequestDataDict,
|
||||||
boundary: str = None,
|
boundary: str = None,
|
||||||
content_type: str = None,
|
content_type: str = None,
|
||||||
) -> Tuple[MultipartEncoder, str]:
|
) -> Tuple['MultipartEncoder', str]:
|
||||||
|
from requests_toolbelt import MultipartEncoder
|
||||||
|
|
||||||
encoder = MultipartEncoder(
|
encoder = MultipartEncoder(
|
||||||
fields=data.items(),
|
fields=data.items(),
|
||||||
boundary=boundary,
|
boundary=boundary,
|
||||||
|
@ -3,16 +3,20 @@ import mimetypes
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from http.cookiejar import parse_ns_headers
|
from http.cookiejar import parse_ns_headers
|
||||||
|
from pathlib import Path
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple, Callable, Iterable, TypeVar
|
||||||
|
|
||||||
import requests.auth
|
import requests.auth
|
||||||
|
|
||||||
RE_COOKIE_SPLIT = re.compile(r', (?=[^ ;]+=)')
|
RE_COOKIE_SPLIT = re.compile(r', (?=[^ ;]+=)')
|
||||||
Item = Tuple[str, Any]
|
Item = Tuple[str, Any]
|
||||||
Items = List[Item]
|
Items = List[Item]
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class JsonDictPreservingDuplicateKeys(OrderedDict):
|
class JsonDictPreservingDuplicateKeys(OrderedDict):
|
||||||
@ -207,3 +211,29 @@ def parse_content_type_header(header):
|
|||||||
value = param[index_of_equals + 1:].strip(items_to_strip)
|
value = param[index_of_equals + 1:].strip(items_to_strip)
|
||||||
params_dict[key.lower()] = value
|
params_dict[key.lower()] = value
|
||||||
return content_type, params_dict
|
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
|
||||||
|
12
setup.py
12
setup.py
@ -33,7 +33,9 @@ install_requires = [
|
|||||||
'requests[socks]>=2.22.0',
|
'requests[socks]>=2.22.0',
|
||||||
'Pygments>=2.5.2',
|
'Pygments>=2.5.2',
|
||||||
'requests-toolbelt>=0.9.1',
|
'requests-toolbelt>=0.9.1',
|
||||||
|
'multidict>=4.7.0',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
|
'importlib-metadata>=1.4.0; python_version < "3.8"',
|
||||||
]
|
]
|
||||||
install_requires_win_only = [
|
install_requires_win_only = [
|
||||||
'colorama>=0.2.4',
|
'colorama>=0.2.4',
|
||||||
@ -69,7 +71,7 @@ setup(
|
|||||||
description=httpie.__doc__.strip(),
|
description=httpie.__doc__.strip(),
|
||||||
long_description=long_description(),
|
long_description=long_description(),
|
||||||
long_description_content_type='text/markdown',
|
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',
|
download_url=f'https://github.com/httpie/httpie/archive/{httpie.__version__}.tar.gz',
|
||||||
author=httpie.__author__,
|
author=httpie.__author__,
|
||||||
author_email='jakub@roztocil.co',
|
author_email='jakub@roztocil.co',
|
||||||
@ -79,9 +81,10 @@ setup(
|
|||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'http = httpie.__main__:main',
|
'http = httpie.__main__:main',
|
||||||
'https = httpie.__main__:main',
|
'https = httpie.__main__:main',
|
||||||
|
'httpie = httpie.manager.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
python_requires='>=3.6',
|
python_requires='>=3.7',
|
||||||
extras_require=extras_require,
|
extras_require=extras_require,
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
@ -102,7 +105,8 @@ setup(
|
|||||||
project_urls={
|
project_urls={
|
||||||
'GitHub': 'https://github.com/httpie/httpie',
|
'GitHub': 'https://github.com/httpie/httpie',
|
||||||
'Twitter': 'https://twitter.com/httpie',
|
'Twitter': 'https://twitter.com/httpie',
|
||||||
'Documentation': 'https://httpie.org/docs',
|
'Discord': 'https://httpie.io/discord',
|
||||||
'Online Demo': 'https://httpie.org/run',
|
'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-unixsocket
|
||||||
python -m pip install httpie-snapdsocket
|
python -m pip install httpie-snapdsocket
|
||||||
|
|
||||||
echo "Removing no more needed modules ..."
|
|
||||||
python -m pip uninstall -y pip wheel
|
|
||||||
|
|
||||||
override-prime: |
|
override-prime: |
|
||||||
snapcraftctl prime
|
snapcraftctl prime
|
||||||
|
|
||||||
echo "Removing useless files ..."
|
echo "Removing useless files ..."
|
||||||
packages=$SNAPCRAFT_PRIME/lib/python3.8/site-packages
|
packages=$SNAPCRAFT_PRIME/lib/python3.8/site-packages
|
||||||
rm -rfv $packages/_distutils_hack
|
|
||||||
rm -rfv $packages/pkg_resources/tests
|
rm -rfv $packages/pkg_resources/tests
|
||||||
rm -rfv $packages/requests_unixsocket/test*
|
rm -rfv $packages/requests_unixsocket/test*
|
||||||
rm -rfv $packages/setuptools
|
|
||||||
|
|
||||||
echo "Compiling pyc files ..."
|
echo "Compiling pyc files ..."
|
||||||
python -m compileall -f $packages
|
python -m compileall -f $packages
|
||||||
|
|
||||||
echo "Copying extra files ..."
|
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:
|
plugs:
|
||||||
dot-config-httpie:
|
dot-config-httpie:
|
||||||
@ -102,13 +97,19 @@ apps:
|
|||||||
- home
|
- home
|
||||||
- network
|
- network
|
||||||
- removable-media
|
- removable-media
|
||||||
completer: bin/httpie-completion.bash
|
completer: httpie-completion.bash
|
||||||
environment:
|
environment:
|
||||||
LC_ALL: C.UTF-8
|
LC_ALL: C.UTF-8
|
||||||
|
|
||||||
https:
|
https:
|
||||||
command: bin/https
|
command: bin/https
|
||||||
plugs: *plugs
|
plugs: *plugs
|
||||||
completer: bin/httpie-completion.bash
|
completer: httpie-completion.bash
|
||||||
|
environment:
|
||||||
|
LC_ALL: C.UTF-8
|
||||||
|
|
||||||
|
httpie:
|
||||||
|
command: bin/httpie
|
||||||
|
plugs: *plugs
|
||||||
environment:
|
environment:
|
||||||
LC_ALL: C.UTF-8
|
LC_ALL: C.UTF-8
|
||||||
|
@ -5,6 +5,15 @@ import pytest
|
|||||||
from pytest_httpbin import certs
|
from pytest_httpbin import certs
|
||||||
|
|
||||||
from .utils import HTTPBIN_WITH_CHUNKED_SUPPORT_DOMAIN, HTTPBIN_WITH_CHUNKED_SUPPORT
|
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)
|
@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).
|
# line would be escaped).
|
||||||
FILE_CONTENT = FILE_PATH.read_text(encoding=UTF8).strip()
|
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)
|
JSON_FILE_CONTENT = JSON_FILE_PATH.read_text(encoding=UTF8)
|
||||||
BIN_FILE_CONTENT = BIN_FILE_PATH.read_bytes()
|
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?>
|
<?pi data?>
|
||||||
<!-- comment -->
|
<!-- comment -->
|
||||||
<root xmlns="namespace">
|
<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/>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user