forked from extern/httpie-cli
Compare commits
475 Commits
1.0.2
...
mickael/os
Author | SHA1 | Date | |
---|---|---|---|
279e387d86 | |||
50f57f8c82 | |||
555afef486 | |||
476eb4f0d9 | |||
3869c3ce99 | |||
17f74f10f3 | |||
1e094d0a79 | |||
bd227c0364 | |||
9dda23a322 | |||
ef4fa20ceb | |||
7e0bed4e54 | |||
e1627803fe | |||
f954c9e2b7 | |||
80e83f0463 | |||
4f1c9441c5 | |||
7989e438d2 | |||
93114072c8 | |||
08751d3672 | |||
0c9d701618 | |||
a3fa016428 | |||
9c52449344 | |||
e4e4927567 | |||
031b4b89e3 | |||
e1c08a3de5 | |||
033798adc1 | |||
6a4e985f71 | |||
a6c70334cf | |||
7388401134 | |||
4ef31ecf71 | |||
2423f893e5 | |||
b6a694afbc | |||
71adcd97d0 | |||
b50f9aa7e7 | |||
fe96b2af20 | |||
727b8a2c05 | |||
9c89c703ae | |||
8f8851f1db | |||
bce2b3a98e | |||
474093acdf | |||
1535d0c976 | |||
cae83b3f9e | |||
507514b795 | |||
d7ed45bbcd | |||
e6c5cd3e4b | |||
273134123a | |||
529aa78ee1 | |||
e2ba214ac0 | |||
9dd0203bae | |||
ba6fd0bc14 | |||
8f7f4a6ef4 | |||
9984447f18 | |||
10081b9fcc | |||
4f84362d73 | |||
2b5f8f48bf | |||
a51068a44d | |||
f06d870012 | |||
0115a4a466 | |||
7c1d26a8fa | |||
7734e47280 | |||
30c595b770 | |||
b38352858f | |||
a45b94fda6 | |||
513e5080e4 | |||
7c9f415107 | |||
4c8633c6e5 | |||
4d7d6b66cf | |||
a586fca246 | |||
978258ec5b | |||
84ef9f588c | |||
cf21790411 | |||
1ef127c61d | |||
4eaa4d67c5 | |||
9764cc74a4 | |||
778360cde1 | |||
60a7ed4e7b | |||
185af7c9f1 | |||
7e9e7c783f | |||
6039bd8582 | |||
e7d8b9cece | |||
a62391e789 | |||
41666d897f | |||
71008bbedb | |||
85110643e7 | |||
fdd486415a | |||
6c501d23c3 | |||
d10e108b5f | |||
8618f12fce | |||
dac0d716c1 | |||
0340f8caf5 | |||
d7caeaf372 | |||
54c8612452 | |||
4ff22defe4 | |||
c50e287c57 | |||
6bd6648545 | |||
6633b5ae9b | |||
c6cbc7dfa5 | |||
11399dde76 | |||
da47e37c44 | |||
a66af2497a | |||
a94d6d807c | |||
de13423839 | |||
04d05a8abd | |||
2f8d7f77bd | |||
aee77a23af | |||
64c31d554a | |||
41c251ec7c | |||
147a066dbe | |||
b7300c1096 | |||
5d4e7a9a18 | |||
5717fb1ad5 | |||
9c38da96b0 | |||
e5bda98ee7 | |||
c8d70e8c0b | |||
ae6f57dc76 | |||
b4c94e0f26 | |||
7ceb313ccf | |||
3228e74df5 | |||
dc4309771e | |||
07a0359316 | |||
2d55c01c7e | |||
1470ca0c77 | |||
9857693ebf | |||
da03a0656e | |||
61f1ffd0eb | |||
9792513c68 | |||
350f973f70 | |||
8d35a12d27 | |||
8374a9ed83 | |||
a61f9e1114 | |||
611b278b63 | |||
175e36da6b | |||
19e1e26d97 | |||
9b5aedb02d | |||
3865fabf09 | |||
355befcbfc | |||
fc7a349d36 | |||
06ef27c576 | |||
0e556ec3a8 | |||
464b5b4c1d | |||
264d45cdf5 | |||
0ff0874fa3 | |||
39314887c4 | |||
f9a488d47e | |||
0001297f41 | |||
e2d43c14ce | |||
a3a08a9a22 | |||
7cbdf2c608 | |||
1274d869f6 | |||
611bcdaab1 | |||
fc45bf0fe3 | |||
56c4ba1794 | |||
8f83bfe767 | |||
a32ad344dd | |||
c53fbe5ae3 | |||
070ba9fa5a | |||
82ee071362 | |||
97dffb35a2 | |||
18af03ac18 | |||
904dd4107a | |||
8efabc86e6 | |||
cc20488f49 | |||
b918972862 | |||
84c7327057 | |||
e944dbd7fa | |||
157f3a1840 | |||
61dbadb730 | |||
7be25d0751 | |||
5d5a8b4091 | |||
bb36897054 | |||
173e622567 | |||
3426030370 | |||
d014498713 | |||
5414d1853e | |||
1ac8f69651 | |||
3c07a25326 | |||
cf78a12e46 | |||
0f1e098cc4 | |||
0401d7b31c | |||
795627f965 | |||
21cc008cb2 | |||
77b8c37cb0 | |||
db685d58b5 | |||
44ae67685d | |||
6922a0c912 | |||
2afdc958c6 | |||
57b1baf1d1 | |||
1828da6a50 | |||
0629f2ff42 | |||
d71b7eee81 | |||
9883a46575 | |||
2409077a6d | |||
02971b938d | |||
f7e77efe4b | |||
5d8bd0da7c | |||
3c6e7c73fe | |||
d64c0ee415 | |||
311a5ede70 | |||
f64c90010f | |||
8456ddb27c | |||
cf254680b7 | |||
42c4a7596b | |||
1573058811 | |||
51bc8fb2c6 | |||
a69d6f44fd | |||
507cd6e255 | |||
759e4400d0 | |||
8cb1af7376 | |||
2f8d330b57 | |||
32d8b481e9 | |||
75f1e02215 | |||
70ba84dc48 | |||
5a5b42340f | |||
299250b3c3 | |||
6925d930da | |||
c1948f8340 | |||
b80ba040ac | |||
b7754f92ce | |||
e4e40e5b06 | |||
d12af4a569 | |||
c431ed7728 | |||
16ef08a159 | |||
100872b5cf | |||
664cebfbcc | |||
743f9738a3 | |||
69445c106c | |||
1813cf6156 | |||
a23b0e39e5 | |||
06dec4e6c6 | |||
ce185bd0fa | |||
1e1dbfeba0 | |||
5a908aa411 | |||
6cd934d1b8 | |||
d32c8cab12 | |||
5ce7c190e9 | |||
1aa1366f99 | |||
2c7f24e3e5 | |||
c90d039a0b | |||
ae22d4e754 | |||
69e1067a2c | |||
7e38f9ccf0 | |||
d546081340 | |||
6421c145d9 | |||
61e7cd786e | |||
4bd2e622a5 | |||
a4a1e8d43b | |||
ebf2139fd5 | |||
6c84cebed4 | |||
10246366da | |||
a448b0d928 | |||
0541490dda | |||
3704db9b6d | |||
d1665b08d2 | |||
1a4e0c2646 | |||
0d480139e4 | |||
9931747901 | |||
8891afa3b7 | |||
4f493d51f8 | |||
cf937b6b79 | |||
14677bd25d | |||
49e71d252f | |||
d6f25b1017 | |||
a434cddd42 | |||
55d7af86fd | |||
978aace86c | |||
ecdeffe7c8 | |||
9500ce136a | |||
93d07cfe57 | |||
5945845420 | |||
3ee5b49256 | |||
bb024757b6 | |||
d35864e79d | |||
8a106781be | |||
23dd80563f | |||
2bab69d9fb | |||
826489950d | |||
b86598886e | |||
c240162cab | |||
26e29612f2 | |||
37200eb055 | |||
9c68d7dd87 | |||
7ee519ef46 | |||
c4627cc882 | |||
492687b0da | |||
caeef2fb7c | |||
aae596d472 | |||
cb51faec51 | |||
c2a0cef76e | |||
493e98c833 | |||
ca02e51420 | |||
cd085cbc0d | |||
27d57ce773 | |||
4c4efff56a | |||
a53505f26e | |||
165dc36f8d | |||
5df3a91619 | |||
7dbceafc01 | |||
d62d6a77d1 | |||
0a81facccf | |||
3e20ade645 | |||
0c47094109 | |||
defe4bc76d | |||
afee6a7970 | |||
7b676dd583 | |||
5af0874ed3 | |||
e11a2d1346 | |||
b2044fc18d | |||
d9a2d665ad | |||
e83e275dff | |||
4a99495466 | |||
495f67229a | |||
45b9bae3dc | |||
774ff148cd | |||
70a78249c1 | |||
fc85988368 | |||
83bd8059de | |||
3af5f1f305 | |||
4351650691 | |||
770976a66e | |||
29b692d597 | |||
8936d1b71e | |||
4f32b76223 | |||
c9d770017e | |||
cdf691c212 | |||
684a4708d7 | |||
5754e33a75 | |||
14fe7dbb27 | |||
3a6ac7d126 | |||
e9080e6b22 | |||
c73858b9c3 | |||
7340b2b64d | |||
8d246415fd | |||
381dd4f619 | |||
e6bad645ed | |||
6e9cd139a6 | |||
deee2dffd0 | |||
c3be722188 | |||
a7e5228712 | |||
5d628756ab | |||
364edc4bd8 | |||
ce5ca6c480 | |||
4b524e6a8c | |||
e4a3ce8b9d | |||
348cc7d5c5 | |||
ab3ea24630 | |||
cd5116705c | |||
38bc578744 | |||
1bc54d4cb4 | |||
fe8b547cc7 | |||
5aa9ed795e | |||
c82d9b629f | |||
e8b22d8b51 | |||
585cc0c039 | |||
615d887513 | |||
89faec994a | |||
490eeaa650 | |||
f1ab816ecd | |||
6e2c31a5a9 | |||
0608b5869f | |||
fcc3aaf873 | |||
dcd6b63e45 | |||
ab2bda3ffe | |||
7390869cd6 | |||
0af486d1b7 | |||
6cb822255d | |||
f202f338a4 | |||
f0058eeaee | |||
a23b636a63 | |||
fc497daf7d | |||
b48ba74ce2 | |||
9bae27354e | |||
d9b3a16fa6 | |||
f031b8cc8b | |||
2dbafe27ed | |||
3affc245c4 | |||
85da430d16 | |||
a42b275ae2 | |||
37fa67cd3c | |||
0df4db7bb4 | |||
374c371ef1 | |||
64c81fc2ec | |||
0252c2642e | |||
b53ace480a | |||
79b0f65fef | |||
ed6156084f | |||
92fe452f92 | |||
0169151aa3 | |||
525449f044 | |||
3c4a5e7304 | |||
d9aadeef51 | |||
2bb54da368 | |||
3fa583e591 | |||
b7767b3c62 | |||
a5d9a839e5 | |||
2ffd8d9d9b | |||
7f80408945 | |||
3ec5c4a643 | |||
3909a436a9 | |||
a77f660ba7 | |||
548857f35a | |||
8741438484 | |||
3176785a5f | |||
c8fd4c2d6e | |||
99f8a8c23d | |||
f866778421 | |||
5a4392076a | |||
bece3c77bb | |||
c946b3d34f | |||
45e8e4e4ea | |||
bd3208cf24 | |||
4dffac7a25 | |||
a34b3d9d87 | |||
30624e66ec | |||
d603502960 | |||
09cd85918e | |||
b947d4826a | |||
e8ef5a783f | |||
82a224a658 | |||
9da5c41704 | |||
224519e0e2 | |||
aba3b1ec01 | |||
466df77b6b | |||
3ea75a3577 | |||
3e24827f4d | |||
1dc67a6a38 | |||
a5713f7190 | |||
0f654388fc | |||
63df735fef | |||
2579827418 | |||
9bd8b4e8f7 | |||
d998013655 | |||
ced9212c1f | |||
07da8ea852 | |||
8e04a24b90 | |||
8512a630f9 | |||
2da2cec83c | |||
a4d8f1f22e | |||
5ec954c03d | |||
2deaccf2d1 | |||
46c4f4e225 | |||
2d16494845 | |||
bb4f101c1e | |||
82081c889b | |||
05fc9c480a | |||
e93de1fbe7 | |||
a969013bdd | |||
65601f09b2 | |||
0f439a5dab | |||
b3d2c1876e | |||
c297af0012 | |||
f27b626a96 | |||
c1d5a4a109 | |||
db3016a602 | |||
4dd9dbd314 | |||
29df4cd4f3 | |||
4d299a5531 | |||
add6601009 | |||
fa96041ec8 | |||
3dccb2e325 | |||
0a0de1755e | |||
747be30d2e | |||
88a9583f4c | |||
c5ca9d248e | |||
fd6e87914c | |||
6dee49357d | |||
df36d6255d | |||
e92b831e6e | |||
fd44f1af93 | |||
b6309547d5 | |||
3a46149de1 | |||
b7c8bf0800 | |||
69d010a11b | |||
42ff243400 | |||
933b438e5f | |||
358342d1c9 | |||
c591a3810d |
@ -1,4 +1,4 @@
|
||||
# http://editorconfig.org
|
||||
# https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a possible bug in HTTPie
|
||||
title: ''
|
||||
labels: "new, bug"
|
||||
assignees: 'BoboTiG'
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I've searched for similar issues.
|
||||
- [ ] I'm using the latest version of HTTPie.
|
||||
|
||||
---
|
||||
|
||||
## Minimal reproduction code and steps
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Current result
|
||||
|
||||
…
|
||||
|
||||
## Expected result
|
||||
|
||||
…
|
||||
|
||||
---
|
||||
|
||||
## Debug output
|
||||
|
||||
Please re-run the command with `--debug`, then copy the entire command & output and paste both below:
|
||||
|
||||
```bash
|
||||
$ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
|
||||
<COMPLETE OUTPUT>
|
||||
```
|
||||
|
||||
## Additional information, screenshots, or code examples
|
||||
|
||||
…
|
30
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an enhancement for HTTPie
|
||||
title: ''
|
||||
labels: "new, enhancement"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I've searched for similar feature requests.
|
||||
|
||||
---
|
||||
|
||||
## Enhancement request
|
||||
|
||||
…
|
||||
|
||||
---
|
||||
|
||||
## Problem it solves
|
||||
|
||||
E.g. “I'm always frustrated when […]”, “I’m trying to do […] so that […]”.
|
||||
|
||||
---
|
||||
|
||||
## Additional information, screenshots, or code examples
|
||||
|
||||
…
|
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Other
|
||||
about: Anything else that isn't a feature or a bug
|
||||
title: ''
|
||||
labels: "new"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
If you have a general question, please consider asking on Discord: https://httpie.io/chat
|
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
# GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
assignees:
|
||||
- BoboTiG
|
19
.github/workflows/code-style.yml
vendored
Normal file
19
.github/workflows/code-style.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/code-style.yml
|
||||
- extras/*.py
|
||||
- httpie/**/*.py
|
||||
- setup.py
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
code-style:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make venv
|
||||
- run: make codestyle
|
22
.github/workflows/coverage.yml
vendored
Normal file
22
.github/workflows/coverage.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/coverage.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- run: make install
|
||||
- run: make test-cover
|
||||
- run: make codecov-upload
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_REPO_TOKEN }}
|
||||
- run: make test-dist
|
19
.github/workflows/docs-check-markdown.yml
vendored
Normal file
19
.github/workflows/docs-check-markdown.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "*.md"
|
||||
- "**/*.md"
|
||||
|
||||
jobs:
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- name: Install the linter
|
||||
run: sudo gem install mdl
|
||||
- name: Check files
|
||||
run: make doc-check
|
20
.github/workflows/docs-deploy.yml
vendored
Normal file
20
.github/workflows/docs-deploy.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- docs/README.md
|
||||
- docs/config.json
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- unpublished
|
||||
- deleted
|
||||
jobs:
|
||||
trigger-doc-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install HTTPie
|
||||
run: sudo snap install --edge httpie
|
||||
- name: Trigger new documentation build
|
||||
run: http --ignore-stdin POST ${{ secrets.DOCS_UPDATE_VERCEL_HOOK }}
|
26
.github/workflows/docs-update-install.yml
vendored
Normal file
26
.github/workflows/docs-update-install.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- docs/installation/*
|
||||
|
||||
# Allow to call the workflow manually
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
doc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make install
|
||||
- run: make doc-update-install
|
||||
- uses: Automattic/action-commit-to-branch@master
|
||||
with:
|
||||
branch: master
|
||||
commit_message: Auto-update installation instructions in the docs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
on:
|
||||
# Add a "Trigger" button to manually start the workflow.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch, tag or SHA to release from"
|
||||
required: true
|
||||
default: "master"
|
||||
# It could be fully automated by uncommenting following lines.
|
||||
# Let's see later if we are confident enough to try it :)
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
jobs:
|
||||
new-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: make publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
23
.github/workflows/test-package-linux-snap.yml
vendored
Normal file
23
.github/workflows/test-package-linux-snap.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/test-package-linux-snap.yml
|
||||
- snapcraft.yaml
|
||||
|
||||
jobs:
|
||||
snap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
uses: snapcore/action-build@v1
|
||||
id: snapcraft
|
||||
- name: Install
|
||||
run: sudo snap install --dangerous ${{ steps.snapcraft.outputs.snap }}
|
||||
- name: Test
|
||||
run: |
|
||||
httpie.http --version
|
||||
httpie.https --version
|
||||
# Auto-aliases cannot be tested when installing a snap outside the store.
|
||||
# http --version
|
||||
# https --version
|
17
.github/workflows/test-package-mac-brew.yml
vendored
Normal file
17
.github/workflows/test-package-mac-brew.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/test-package-mac-brew.yml
|
||||
- docs/packaging/brew/httpie.rb
|
||||
|
||||
jobs:
|
||||
brew:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup brew
|
||||
run: |
|
||||
brew developer on
|
||||
brew update
|
||||
- name: Build and test the formula
|
||||
run: make brew-test
|
45
.github/workflows/tests.yml
vendored
Normal file
45
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/tests.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/tests.yml
|
||||
- httpie/**/*.py
|
||||
- setup.*
|
||||
- tests/**/*.py
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9, "3.10"]
|
||||
pyopenssl: [0, 1]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Windows setup
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install --upgrade '.[dev]'
|
||||
python -m pytest --verbose ./httpie ./tests
|
||||
env:
|
||||
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
|
||||
- name: Linux & Mac setup
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
make install
|
||||
make test
|
||||
env:
|
||||
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
|
152
.gitignore
vendored
152
.gitignore
vendored
@ -1,13 +1,153 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
__pycache__/
|
||||
dist/
|
||||
build/
|
||||
*.egg-info
|
||||
.cache/
|
||||
.tox/
|
||||
.coverage
|
||||
*.pyc
|
||||
*.egg
|
||||
htmlcov
|
||||
|
||||
|
||||
##############################################################################
|
||||
# The below is GitHub template for Python project. gitignore.
|
||||
# <https://github.com/github/gitignore/blob/master/Python.gitignore>
|
||||
##############################################################################
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
venv*/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Packit
|
||||
/httpie.spec
|
||||
/httpie-*.rpm
|
||||
/httpie-*.tar.gz
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# Windows Chocolatey
|
||||
*.nupkg
|
||||
|
19
.packit.yaml
Normal file
19
.packit.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
# See the documentation for more information:
|
||||
# https://packit.dev/docs/configuration/
|
||||
specfile_path: httpie.spec
|
||||
actions:
|
||||
# get the current Fedora Rawhide specfile:
|
||||
post-upstream-clone: "wget https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -O httpie.spec"
|
||||
jobs:
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-all
|
||||
additional_repos:
|
||||
- "https://kojipkgs.fedoraproject.org/repos/f$releasever-build/latest/$basearch/"
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
metadata:
|
||||
dist_git_branches:
|
||||
- rawhide
|
96
.travis.yml
96
.travis.yml
@ -1,96 +0,0 @@
|
||||
# <https://travis-ci.org/jakubroztocil/httpie>
|
||||
sudo: false
|
||||
language: python
|
||||
os:
|
||||
- linux
|
||||
env:
|
||||
global:
|
||||
- NEWEST_PYTHON=3.7
|
||||
python:
|
||||
# <https://docs.travis-ci.com/user/languages/python/>
|
||||
|
||||
- 2.7
|
||||
|
||||
# Python 3.4 fails installing packages
|
||||
# <https://travis-ci.org/jakubroztocil/httpie/jobs/403263566#L636>
|
||||
# - 3.4
|
||||
|
||||
- 3.5
|
||||
- 3.6
|
||||
# - 3.7 # is done in the matrix below as described in travis-ci/travis-ci#9069
|
||||
- pypy
|
||||
|
||||
# pypy3 currently fails because of a Flask issue
|
||||
# - pypy3
|
||||
|
||||
cache: pip
|
||||
matrix:
|
||||
include:
|
||||
# Add manually defined OS X builds
|
||||
# <https://docs.travis-ci.com/user/multi-os/#Python-example-(unsupported-languages)>
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
# Stock OSX Python
|
||||
- TOXENV=py27-osx-builtin
|
||||
- BREW_PYTHON_PACKAGE=
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
# Latest Python 2.7 from Homebrew
|
||||
- TOXENV=py27
|
||||
- BREW_PYTHON_PACKAGE=python@2
|
||||
- os: osx
|
||||
language: generic
|
||||
env:
|
||||
# Latest Python 3.x from Homebrew
|
||||
- TOXENV=py37 # <= needs to be kept up-to-date to reflect latest minor version
|
||||
- BREW_PYTHON_PACKAGE=python@3
|
||||
# Travis Python 3.7 must run sudo on
|
||||
- os: linux
|
||||
python: 3.7
|
||||
env: TOXENV=py37
|
||||
sudo: true # Required for Python 3.7
|
||||
dist: xenial # Required for Python 3.7
|
||||
# Add a codestyle-only build
|
||||
- os: linux
|
||||
python: 3.6
|
||||
env: CODESTYLE_ONLY=true
|
||||
install:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'osx' ]]; then
|
||||
if [[ -n "$BREW_PYTHON_PACKAGE" ]]; then
|
||||
brew update
|
||||
if ! brew list --versions "$BREW_PYTHON_PACKAGE" >/dev/null; then
|
||||
brew install "$BREW_PYTHON_PACKAGE"
|
||||
elif ! brew outdated "$BREW_PYTHON_PACKAGE"; then
|
||||
brew upgrade "$BREW_PYTHON_PACKAGE"
|
||||
fi
|
||||
fi
|
||||
sudo pip2 install tox
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
if [[ $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
if [[ $CODESTYLE_ONLY ]]; then
|
||||
make pycodestyle
|
||||
else
|
||||
make test
|
||||
fi
|
||||
else
|
||||
PATH="/usr/local/bin:$PATH" tox -e "$TOXENV"
|
||||
fi
|
||||
after_success:
|
||||
- |
|
||||
if [[ $TRAVIS_PYTHON_VERSION == $NEWEST_PYTHON && $TRAVIS_OS_NAME == 'linux' ]]; then
|
||||
make coveralls
|
||||
fi
|
||||
notifications:
|
||||
webhooks:
|
||||
# options: [always|never|change] default: always
|
||||
on_success: always
|
||||
on_failure: always
|
||||
on_start: always
|
||||
urls:
|
||||
# https://gitter.im/jkbrzt/httpie
|
||||
- https://webhooks.gitter.im/e/c42fcd359a110d02830b
|
41
AUTHORS.md
Normal file
41
AUTHORS.md
Normal file
@ -0,0 +1,41 @@
|
||||
# HTTPie authors
|
||||
|
||||
- [Jakub Roztocil](https://github.com/jakubroztocil)
|
||||
|
||||
## Patches, features, ideas
|
||||
|
||||
[Complete list of contributors on GitHub](https://github.com/httpie/httpie/graphs/contributors)
|
||||
|
||||
- [Cláudia T. Delgado](https://github.com/claudiatd)
|
||||
- [Hank Gay](https://github.com/gthank)
|
||||
- [Jake Basile](https://github.com/jakebasile)
|
||||
- [Vladimir Berkutov](https://github.com/dair-targ)
|
||||
- [Jakob Kramer](https://github.com/gandaro)
|
||||
- [Chris Faulkner](https://github.com/faulkner)
|
||||
- [Alen Mujezinovic](https://github.com/flashingpumpkin)
|
||||
- [Praful Mathur](https://github.com/tictactix)
|
||||
- [Marc Abramowitz](https://github.com/msabramo)
|
||||
- [Ismail Badawi](https://github.com/isbadawi)
|
||||
- [Laurent Bachelier](https://github.com/laurentb)
|
||||
- [Isman Firmansyah](https://github.com/iromli)
|
||||
- [Simon Olofsson](https://github.com/simono)
|
||||
- [Churkin Oleg](https://github.com/Bahus)
|
||||
- [Jökull Sólberg Auðunsson](https://github.com/jokull)
|
||||
- [Matthew M. Boedicker](https://github.com/mmb)
|
||||
- [marblar](https://github.com/marblar)
|
||||
- [Tomek Wójcik](https://github.com/tomekwojcik)
|
||||
- [Davey Shafik](https://github.com/dshafik)
|
||||
- [cido](https://github.com/cido)
|
||||
- [Justin Bonnar](https://github.com/jargonjustin)
|
||||
- [Nathan LaFreniere](https://github.com/nlf)
|
||||
- [Matthias Lehmann](https://github.com/matleh)
|
||||
- [Dennis Brakhane](https://github.com/brakhane)
|
||||
- [Matt Layman](https://github.com/mblayman)
|
||||
- [Edward Yang](https://github.com/honorabrutroll)
|
||||
- [Aleksandr Vinokurov](https://github.com/aleksandr-vin)
|
||||
- [Jeff Byrnes](https://github.com/jeffbyrnes)
|
||||
- [Denis Belavin](https://github.com/LuckyDenis)
|
||||
- [Mickaël Schoentgen](https://github.com/BoboTiG)
|
||||
- [Elena Lape](https://github.com/elenalape)
|
||||
- [Rohit Sehgal](https://github.com/r0hi7)
|
||||
- [Bartłomiej Jacak](https://github.com/bartekjacak)
|
40
AUTHORS.rst
40
AUTHORS.rst
@ -1,40 +0,0 @@
|
||||
==============
|
||||
HTTPie authors
|
||||
==============
|
||||
|
||||
* `Jakub Roztocil <https://github.com/jakubroztocil>`_
|
||||
|
||||
|
||||
Patches and ideas
|
||||
-----------------
|
||||
|
||||
`Complete list of contributors on GitHub <https://github.com/jakubroztocil/httpie/graphs/contributors>`_
|
||||
|
||||
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
|
||||
* `Hank Gay <https://github.com/gthank>`_
|
||||
* `Jake Basile <https://github.com/jakebasile>`_
|
||||
* `Vladimir Berkutov <https://github.com/dair-targ>`_
|
||||
* `Jakob Kramer <https://github.com/gandaro>`_
|
||||
* `Chris Faulkner <https://github.com/faulkner>`_
|
||||
* `Alen Mujezinovic <https://github.com/flashingpumpkin>`_
|
||||
* `Praful Mathur <https://github.com/tictactix>`_
|
||||
* `Marc Abramowitz <https://github.com/msabramo>`_
|
||||
* `Ismail Badawi <https://github.com/isbadawi>`_
|
||||
* `Laurent Bachelier <https://github.com/laurentb>`_
|
||||
* `Isman Firmansyah <https://github.com/iromli>`_
|
||||
* `Simon Olofsson <https://github.com/simono>`_
|
||||
* `Churkin Oleg <https://github.com/Bahus>`_
|
||||
* `Jökull Sólberg Auðunsson <https://github.com/jokull>`_
|
||||
* `Matthew M. Boedicker <https://github.com/mmb>`_
|
||||
* `marblar <https://github.com/marblar>`_
|
||||
* `Tomek Wójcik <https://github.com/tomekwojcik>`_
|
||||
* `Davey Shafik <https://github.com/dshafik>`_
|
||||
* `cido <https://github.com/cido>`_
|
||||
* `Justin Bonnar <https://github.com/jargonjustin>`_
|
||||
* `Nathan LaFreniere <https://github.com/nlf>`_
|
||||
* `Matthias Lehmann <https://github.com/matleh>`_
|
||||
* `Dennis Brakhane <https://github.com/brakhane>`_
|
||||
* `Matt Layman <https://github.com/mblayman>`_
|
||||
* `Edward Yang <https://github.com/honorabrutroll>`_
|
||||
|
||||
|
378
CHANGELOG.md
Normal file
378
CHANGELOG.md
Normal file
@ -0,0 +1,378 @@
|
||||
# Change Log
|
||||
|
||||
This document records all notable changes to [HTTPie](https://httpie.io).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [2.6.0.dev0](https://github.com/httpie/httpie/compare/2.5.0...master) (unreleased)
|
||||
|
||||
- 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 `--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))
|
||||
- Improved handling of responses with incorrect `Content-Type`. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
|
||||
- Installed plugins are now listed in `--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))
|
||||
|
||||
## [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)
|
||||
|
||||
- Added `--raw` to allow specifying the raw request body without extra processing as
|
||||
an alternative to `stdin`. ([#534](https://github.com/httpie/httpie/issues/534))
|
||||
- Added support for XML formatting. ([#1129](https://github.com/httpie/httpie/issues/1129))
|
||||
- Added internal support for file-like object responses to improve adapter plugin support. ([#1094](https://github.com/httpie/httpie/issues/1094))
|
||||
- Fixed `--continue --download` with a single byte to be downloaded left. ([#1032](https://github.com/httpie/httpie/issues/1032))
|
||||
- Fixed `--verbose` HTTP 307 redirects with streamed request body. ([#1088](https://github.com/httpie/httpie/issues/1088))
|
||||
- Fixed handling of session files with `Cookie:` followed by other headers. ([#1126](https://github.com/httpie/httpie/issues/1126))
|
||||
|
||||
## [2.4.0](https://github.com/httpie/httpie/compare/2.3.0...2.4.0) (2021-02-06)
|
||||
|
||||
- Added support for `--session` cookie expiration based on `Set-Cookie: max-age=<n>`. ([#1029](https://github.com/httpie/httpie/issues/1029))
|
||||
- Show a `--check-status` warning with `--quiet` as well, not only when the output is redirected. ([#1026](https://github.com/httpie/httpie/issues/1026))
|
||||
- Fixed upload with `--session` ([#1020](https://github.com/httpie/httpie/issues/1020)).
|
||||
- Fixed a missing blank line between request and response ([#1006](https://github.com/httpie/httpie/issues/1006)).
|
||||
|
||||
## [2.3.0](https://github.com/httpie/httpie/compare/2.2.0...2.3.0) (2020-10-25)
|
||||
|
||||
- Added support for streamed uploads ([#201](https://github.com/httpie/httpie/issues/201)).
|
||||
- Added support for multipart upload streaming ([#684](https://github.com/httpie/httpie/issues/684)).
|
||||
- Added support for body-from-file upload streaming (`http pie.dev/post @file`).
|
||||
- Added `--chunked` to enable chunked transfer encoding ([#753](https://github.com/httpie/httpie/issues/753)).
|
||||
- Added `--multipart` to allow `multipart/form-data` encoding for non-file `--form` requests as well.
|
||||
- Added support for preserving field order in multipart requests ([#903](https://github.com/httpie/httpie/issues/903)).
|
||||
- Added `--boundary` to allow a custom boundary string for `multipart/form-data` requests.
|
||||
- Added support for combining cookies specified on the CLI and in a session file ([#932](https://github.com/httpie/httpie/issues/932)).
|
||||
- Added out of the box SOCKS support with no extra installation ([#904](https://github.com/httpie/httpie/issues/904)).
|
||||
- Added `--quiet, -q` flag to enforce silent behaviour.
|
||||
- Fixed the handling of invalid `expires` dates in `Set-Cookie` headers ([#963](https://github.com/httpie/httpie/issues/963)).
|
||||
- Removed Tox testing entirely ([#943](https://github.com/httpie/httpie/issues/943)).
|
||||
|
||||
## [2.2.0](https://github.com/httpie/httpie/compare/2.1.0...2.2.0) (2020-06-18)
|
||||
|
||||
- Added support for custom content types for uploaded files ([#668](https://github.com/httpie/httpie/issues/668)).
|
||||
- Added support for `$XDG_CONFIG_HOME` ([#920](https://github.com/httpie/httpie/issues/920)).
|
||||
- Added support for `Set-Cookie`-triggered cookie expiration ([#853](https://github.com/httpie/httpie/issues/853)).
|
||||
- Added `--format-options` to allow disabling sorting, etc. ([#128](https://github.com/httpie/httpie/issues/128))
|
||||
- Added `--sorted` and `--unsorted` shortcuts for (un)setting all sorting-related `--format-options`. ([#128](https://github.com/httpie/httpie/issues/128))
|
||||
- Added `--ciphers` to allow configuring OpenSSL ciphers ([#870](https://github.com/httpie/httpie/issues/870)).
|
||||
- Added `netrc` support for auth plugins. Enabled for `--auth-type=basic`
|
||||
and `digest`, 3rd parties may opt in ([#718](https://github.com/httpie/httpie/issues/718), [#719](https://github.com/httpie/httpie/issues/719), [#852](https://github.com/httpie/httpie/issues/852), [#934](https://github.com/httpie/httpie/issues/934)).
|
||||
- Fixed built-in plugins-related circular imports ([#925](https://github.com/httpie/httpie/issues/925)).
|
||||
|
||||
## [2.1.0](https://github.com/httpie/httpie/compare/2.0.0...2.1.0) (2020-04-18)
|
||||
|
||||
- Added `--path-as-is` to bypass dot segment (`/../` or `/./`)
|
||||
URL squashing ([#895](https://github.com/httpie/httpie/issues/895)).
|
||||
- Changed the default `Accept` header value for JSON requests from
|
||||
`application/json, */*` to `application/json, */*;q=0.5`
|
||||
to clearly indicate preference ([#488](https://github.com/httpie/httpie/issues/488)).
|
||||
- Fixed `--form` file upload mixed with redirected `stdin` error handling
|
||||
([#840](https://github.com/httpie/httpie/issues/840)).
|
||||
|
||||
## [2.0.0](https://github.com/httpie/httpie/compare/1.0.3...2.0.0) (2020-01-12)
|
||||
|
||||
- Removed Python 2.7 support ([EOL Jan 2020](https://www.python.org/doc/sunset-python-2/).
|
||||
- Added `--offline` to allow building an HTTP request and printing it but not
|
||||
actually sending it over the network.
|
||||
- Replaced the old collect-all-then-process handling of HTTP communication
|
||||
with one-by-one processing of each HTTP request or response as they become
|
||||
available. This means that you can see headers immediately,
|
||||
see what is being sent even if the request fails, etc.
|
||||
- Removed automatic config file creation to avoid concurrency issues.
|
||||
- Removed the default 30-second connection `--timeout` limit.
|
||||
- Removed Python’s default limit of 100 response headers.
|
||||
- Added `--max-headers` to allow setting the max header limit.
|
||||
- Added `--compress` to allow request body compression.
|
||||
- Added `--ignore-netrc` to allow bypassing credentials from `.netrc`.
|
||||
- Added `https` alias command with `https://` as the default scheme.
|
||||
- Added `$ALL_PROXY` documentation.
|
||||
- Added type annotations throughout the codebase.
|
||||
- Added `tests/` to the PyPi package for the convenience of
|
||||
downstream package maintainers.
|
||||
- Fixed an error when `stdin` was a closed fd.
|
||||
- Improved `--debug` output formatting.
|
||||
|
||||
## [1.0.3](https://github.com/httpie/httpie/compare/1.0.2...1.0.3) (2019-08-26)
|
||||
|
||||
- Fixed CVE-2019-10751 — the way the output filename is generated for
|
||||
`--download` requests without `--output` resulting in a redirect has
|
||||
been changed to only consider the initial URL as the base for the generated
|
||||
filename, and not the final one. This fixes a potential security issue under
|
||||
the following scenario:
|
||||
|
||||
1. A `--download` request with no explicit `--output` is made (e.g.,
|
||||
`$ http -d example.org/file.txt`), instructing httpie to
|
||||
[generate the output filename](https://httpie.org/doc#downloaded-filename)
|
||||
from the `Content-Disposition` response header, or from the URL if the header
|
||||
is not provided.
|
||||
2. The server handling the request has been modified by an attacker and
|
||||
instead of the expected response the URL returns a redirect to another
|
||||
URL, e.g., `attacker.example.org/.bash_profile`, whose response does
|
||||
not provide a `Content-Disposition` header (i.e., the base for the
|
||||
generated filename becomes `.bash_profile` instead of `file.txt`).
|
||||
3. Your current directory doesn’t already contain `.bash_profile`
|
||||
(i.e., no unique suffix is added to the generated filename).
|
||||
4. You don’t notice the potentially unexpected output filename
|
||||
as reported by httpie in the console output
|
||||
(e.g., `Downloading 100.00 B to ".bash_profile"`).
|
||||
|
||||
Reported by Raul Onitza and Giulio Comi.
|
||||
|
||||
## [1.0.2](https://github.com/httpie/httpie/compare/1.0.1...1.0.2) (2018-11-14)
|
||||
|
||||
- Fixed tests for installation with pyOpenSSL.
|
||||
|
||||
## [1.0.1](https://github.com/httpie/httpie/compare/1.0.0...1.0.1) (2018-11-14)
|
||||
|
||||
- Removed external URL calls from tests.
|
||||
|
||||
## [1.0.0](https://github.com/httpie/httpie/compare/0.9.9...1.0.0) (2018-11-02)
|
||||
|
||||
- Added `--style=auto` which follows the terminal ANSI color styles.
|
||||
- Added support for selecting TLS 1.3 via `--ssl=tls1.3`
|
||||
(available once implemented in upstream libraries).
|
||||
- Added `true`/`false` as valid values for `--verify`
|
||||
(in addition to `yes`/`no`) and the boolean value is case-insensitive.
|
||||
- Changed the default `--style` from `solarized` to `auto` (on Windows it stays `fruity`).
|
||||
- Fixed default headers being incorrectly case-sensitive.
|
||||
- Removed Python 2.6 support.
|
||||
|
||||
## [0.9.9](https://github.com/httpie/httpie/compare/0.9.8...0.9.9) (2016-12-08)
|
||||
|
||||
- Fixed README.
|
||||
|
||||
## [0.9.8](https://github.com/httpie/httpie/compare/0.9.6...0.9.8) (2016-12-08)
|
||||
|
||||
- Extended auth plugin API.
|
||||
- Added exit status code `7` for plugin errors.
|
||||
- Added support for `curses`-less Python installations.
|
||||
- Fixed `REQUEST_ITEM` arg incorrectly being reported as required.
|
||||
- Improved `CTRL-C` interrupt handling.
|
||||
- Added the standard exit status code `130` for keyboard interrupts.
|
||||
|
||||
## [0.9.6](https://github.com/httpie/httpie/compare/0.9.4...0.9.6) (2016-08-13)
|
||||
|
||||
- Added Python 3 as a dependency for Homebrew installations
|
||||
to ensure some of the newer HTTP features work out of the box
|
||||
for macOS users (starting with HTTPie 0.9.4.).
|
||||
- Added the ability to unset a request header with `Header:`, and send an
|
||||
empty value with `Header;`.
|
||||
- Added `--default-scheme <URL_SCHEME>` to enable things like
|
||||
`$ alias https='http --default-scheme=https`.
|
||||
- Added `-I` as a shortcut for `--ignore-stdin`.
|
||||
- Added fish shell completion (located in `extras/httpie-completion.fish`
|
||||
in the GitHub repo).
|
||||
- Updated `requests` to 2.10.0 so that SOCKS support can be added via
|
||||
`pip install requests[socks]`.
|
||||
- Changed the default JSON `Accept` header from `application/json`
|
||||
to `application/json, */*`.
|
||||
- Changed the pre-processing of request HTTP headers so that any leading
|
||||
and trailing whitespace is removed.
|
||||
|
||||
## [0.9.4](https://github.com/httpie/httpie/compare/0.9.3...0.9.4) (2016-07-01)
|
||||
|
||||
- Added `Content-Type` of files uploaded in `multipart/form-data` requests
|
||||
- Added `--ssl=<PROTOCOL>` to specify the desired SSL/TLS protocol version
|
||||
to use for HTTPS requests.
|
||||
- Added JSON detection with `--json, -j` to work around incorrect
|
||||
`Content-Type`
|
||||
- Added `--all` to show intermediate responses such as redirects (with `--follow`)
|
||||
- Added `--history-print, -P WHAT` to specify formatting of intermediate responses
|
||||
- Added `--max-redirects=N` (default 30)
|
||||
- Added `-A` as short name for `--auth-type`
|
||||
- Added `-F` as short name for `--follow`
|
||||
- Removed the `implicit_content_type` config option
|
||||
(use `"default_options": ["--form"]` instead)
|
||||
- Redirected `stdout` doesn't trigger an error anymore when `--output FILE`
|
||||
is set
|
||||
- Changed the default `--style` back to `solarized` for better support
|
||||
of light and dark terminals
|
||||
- Improved `--debug` output
|
||||
- Fixed `--session` when used with `--download`
|
||||
- Fixed `--download` to trim too long filenames before saving the file
|
||||
- Fixed the handling of `Content-Type` with multiple `+subtype` parts
|
||||
- Removed the XML formatter as the implementation suffered from multiple issues
|
||||
|
||||
## [0.9.3](https://github.com/httpie/httpie/compare/0.9.2...0.9.3) (2016-01-01)
|
||||
|
||||
- Changed the default color `--style` from `solarized` to `monokai`
|
||||
- Added basic Bash autocomplete support (need to be installed manually)
|
||||
- Added request details to connection error messages
|
||||
- Fixed `'requests.packages.urllib3' has no attribute 'disable_warnings'`
|
||||
errors that occurred in some installations
|
||||
- Fixed colors and formatting on Windows
|
||||
- Fixed `--auth` prompt on Windows
|
||||
|
||||
## [0.9.2](https://github.com/httpie/httpie/compare/0.9.1...0.9.2) (2015-02-24)
|
||||
|
||||
- Fixed compatibility with Requests 2.5.1
|
||||
- Changed the default JSON `Content-Type` to `application/json` as UTF-8
|
||||
is the default JSON encoding
|
||||
|
||||
## [0.9.1](https://github.com/httpie/httpie/compare/0.9.0...0.9.1) (2015-02-07)
|
||||
|
||||
- Added support for Requests transport adapter plugins
|
||||
(see [httpie-unixsocket](https://github.com/httpie/httpie-unixsocket)
|
||||
and [httpie-http2](https://github.com/httpie/httpie-http2))
|
||||
|
||||
## [0.9.0](https://github.com/httpie/httpie/compare/0.8.0...0.9.0) (2015-01-31)
|
||||
|
||||
- Added `--cert` and `--cert-key` parameters to specify a client side
|
||||
certificate and private key for SSL
|
||||
- Improved unicode support
|
||||
- Improved terminal color depth detection via `curses`
|
||||
- To make it easier to deal with Windows paths in request items, `\`
|
||||
now only escapes special characters (the ones that are used as key-value
|
||||
separators by HTTPie)
|
||||
- Switched from `unittest` to `pytest`
|
||||
- Added Python `wheel` support
|
||||
- Various test suite improvements
|
||||
- Added `CONTRIBUTING`
|
||||
- Fixed `User-Agent` overwriting when used within a session
|
||||
- Fixed handling of empty passwords in URL credentials
|
||||
- Fixed multiple file uploads with the same form field name
|
||||
- Fixed `--output=/dev/null` on Linux
|
||||
- Miscellaneous bugfixes
|
||||
|
||||
## [0.8.0](https://github.com/httpie/httpie/compare/0.7.1...0.8.0) (2014-01-25)
|
||||
|
||||
- Added `field=@file.txt` and `field:=@file.json` for embedding
|
||||
the contents of text and JSON files into request data
|
||||
- Added curl-style shorthand for localhost
|
||||
- Fixed request `Host` header value output so that it doesn't contain
|
||||
credentials, if included in the URL
|
||||
|
||||
## [0.7.1](https://github.com/httpie/httpie/compare/0.6.0...0.7.1) (2013-09-24)
|
||||
|
||||
- Added `--ignore-stdin`
|
||||
- Added support for auth plugins
|
||||
- Improved `--help` output
|
||||
- Improved `Content-Disposition` parsing for `--download` mode
|
||||
- Update to Requests 2.0.0
|
||||
|
||||
## [0.6.0](https://github.com/httpie/httpie/compare/0.5.1...0.6.0) (2013-06-03)
|
||||
|
||||
- XML data is now formatted
|
||||
- `--session` and `--session-read-only` now also accept paths to
|
||||
session files (eg. `http --session=/tmp/session.json example.org`)
|
||||
|
||||
## [0.5.1](https://github.com/httpie/httpie/compare/0.5.0...0.5.1) (2013-05-13)
|
||||
|
||||
- `Content-*` and `If-*` request headers are not stored in sessions
|
||||
anymore as they are request-specific
|
||||
|
||||
## [0.5.0](https://github.com/httpie/httpie/compare/0.4.1...0.5.0) (2013-04-27)
|
||||
|
||||
- Added a download mode via `--download`
|
||||
- Fixes miscellaneous bugs
|
||||
|
||||
## [0.4.1](https://github.com/httpie/httpie/compare/0.4.0...0.4.1) (2013-02-26)
|
||||
|
||||
- Fixed `setup.py`
|
||||
|
||||
## [0.4.0](https://github.com/httpie/httpie/compare/0.3.0...0.4.0) (2013-02-22)
|
||||
|
||||
- Added Python 3.3 compatibility
|
||||
- Added Requests >= v1.0.4 compatibility
|
||||
- Added support for credentials in URL
|
||||
- Added `--no-option` for every `--option` to be config-friendly
|
||||
- Mutually exclusive arguments can be specified multiple times. The
|
||||
last value is used
|
||||
|
||||
## [0.3.0](https://github.com/httpie/httpie/compare/0.2.7...0.3.0) (2012-09-21)
|
||||
|
||||
- Allow output redirection on Windows
|
||||
- Added configuration file
|
||||
- Added persistent session support
|
||||
- Renamed `--allow-redirects` to `--follow`
|
||||
- Improved the usability of `http --help`
|
||||
- Fixed installation on Windows with Python 3
|
||||
- Fixed colorized output on Windows with Python 3
|
||||
- CRLF HTTP header field separation in the output
|
||||
- Added exit status code `2` for timed-out requests
|
||||
- Added the option to separate colorizing and formatting
|
||||
(`--pretty=all`, `--pretty=colors` and `--pretty=format`)
|
||||
`--ugly` has bee removed in favor of `--pretty=none`
|
||||
|
||||
## [0.2.7](https://github.com/httpie/httpie/compare/0.2.5...0.2.7) (2012-08-07)
|
||||
|
||||
- Added compatibility with Requests 0.13.6
|
||||
- Added streamed terminal output. `--stream, -S` can be used to enable
|
||||
streaming also with `--pretty` and to ensure a more frequent output
|
||||
flushing
|
||||
- Added support for efficient large file downloads
|
||||
- Sort headers by name (unless `--pretty=none`)
|
||||
- Response body is fetched only when needed (e.g., not with `--headers`)
|
||||
- Improved content type matching
|
||||
- Updated Solarized color scheme
|
||||
- Windows: Added `--output FILE` to store output into a file
|
||||
(piping results in corrupted data on Windows)
|
||||
- Proper handling of binary requests and responses
|
||||
- Fixed printing of `multipart/form-data` requests
|
||||
- Renamed `--traceback` to `--debug`
|
||||
|
||||
## [0.2.6](https://github.com/httpie/httpie/compare/0.2.5...0.2.6) (2012-07-26)
|
||||
|
||||
- The short option for `--headers` is now `-h` (`-t` has been
|
||||
removed, for usage use `--help`)
|
||||
- Form data and URL parameters can have multiple fields with the same name
|
||||
(e.g.,`http -f url a=1 a=2`)
|
||||
- Added `--check-status` to exit with an error on HTTP 3xx, 4xx and
|
||||
5xx (3, 4, and 5, respectively)
|
||||
- If the output is piped to another program or redirected to a file,
|
||||
the default behaviour is to only print the response body
|
||||
(It can still be overwritten via the `--print` flag.)
|
||||
- Improved highlighting of HTTP headers
|
||||
- Added query string parameters (`param==value`)
|
||||
- Added support for terminal colors under Windows
|
||||
|
||||
## [0.2.5](https://github.com/httpie/httpie/compare/0.2.2...0.2.5) (2012-07-17)
|
||||
|
||||
- Unicode characters in prettified JSON now don't get escaped for
|
||||
improved readability
|
||||
- --auth now prompts for a password if only a username provided
|
||||
- Added support for request payloads from a file path with automatic
|
||||
`Content-Type` (`http URL @/path`)
|
||||
- Fixed missing query string when displaying the request headers via
|
||||
`--verbose`
|
||||
- Fixed Content-Type for requests with no data
|
||||
|
||||
## [0.2.2](https://github.com/httpie/httpie/compare/0.2.1...0.2.2) (2012-06-24)
|
||||
|
||||
- The `METHOD` positional argument can now be omitted (defaults to
|
||||
`GET`, or to `POST` with data)
|
||||
- Fixed --verbose --form
|
||||
- Added support for Tox
|
||||
|
||||
## [0.2.1](https://github.com/httpie/httpie/compare/0.2.0...0.2.1) (2012-06-13)
|
||||
|
||||
- Added compatibility with `requests-0.12.1`
|
||||
- Dropped custom JSON and HTTP lexers in favor of the ones newly included
|
||||
in `pygments-1.5`
|
||||
|
||||
## [0.2.0](https://github.com/httpie/httpie/compare/0.1.6...0.2.0) (2012-04-25)
|
||||
|
||||
- Added Python 3 support
|
||||
- Added the ability to print the HTTP request as well as the response
|
||||
(see `--print` and `--verbose`)
|
||||
- Added support for Digest authentication
|
||||
- Added file upload support
|
||||
(`http -f POST file_field_name@/path/to/file`)
|
||||
- Improved syntax highlighting for JSON
|
||||
- Added support for field name escaping
|
||||
- Many bug fixes
|
||||
|
||||
## [0.1.6](https://github.com/httpie/httpie/compare/0.1.5...0.1.6) (2012-03-04)
|
||||
|
||||
- Fixed `setup.py`
|
||||
|
||||
## [0.1.5](https://github.com/httpie/httpie/compare/0.1.4...0.1.5) (2012-03-04)
|
||||
|
||||
- Many improvements and bug fixes
|
||||
|
||||
## [0.1.4](https://github.com/httpie/httpie/compare/b966efa...0.1.4) (2012-02-28)
|
||||
|
||||
- Many improvements and bug fixes
|
||||
|
||||
## [0.1.0](https://github.com/httpie/httpie/commit/b966efa) (2012-02-25)
|
||||
|
||||
- Initial public release
|
364
CHANGELOG.rst
364
CHANGELOG.rst
@ -1,364 +0,0 @@
|
||||
==========
|
||||
Change Log
|
||||
==========
|
||||
|
||||
This document records all notable changes to `HTTPie <http://httpie.org>`_.
|
||||
This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
||||
|
||||
|
||||
`1.0.3-dev`_ (unreleased)
|
||||
-------------------------
|
||||
|
||||
* No changes yet.
|
||||
|
||||
|
||||
`1.0.2`_ (2018-11-14)
|
||||
-------------------------
|
||||
|
||||
* Fixed tests for installation with pyOpenSSL.
|
||||
|
||||
|
||||
`1.0.1`_ (2018-11-14)
|
||||
-------------------------
|
||||
|
||||
* Removed external URL calls from tests.
|
||||
|
||||
|
||||
`1.0.0`_ (2018-11-02)
|
||||
-------------------------
|
||||
|
||||
* Added ``--style=auto`` which follows the terminal ANSI color styles.
|
||||
* Added support for selecting TLS 1.3 via ``--ssl=tls1.3``
|
||||
(available once implemented in upstream libraries).
|
||||
* Added ``true``/``false`` as valid values for ``--verify``
|
||||
(in addition to ``yes``/``no``) and the boolean value is case-insensitive.
|
||||
* Changed the default ``--style`` from ``solarized`` to ``auto`` (on Windows it stays ``fruity``).
|
||||
* Fixed default headers being incorrectly case-sensitive.
|
||||
* Removed Python 2.6 support.
|
||||
|
||||
|
||||
|
||||
`0.9.9`_ (2016-12-08)
|
||||
---------------------
|
||||
|
||||
* Fixed README.
|
||||
|
||||
|
||||
`0.9.8`_ (2016-12-08)
|
||||
---------------------
|
||||
|
||||
* Extended auth plugin API.
|
||||
* Added exit status code ``7`` for plugin errors.
|
||||
* Added support for ``curses``-less Python installations.
|
||||
* Fixed ``REQUEST_ITEM`` arg incorrectly being reported as required.
|
||||
* Improved ``CTRL-C`` interrupt handling.
|
||||
* Added the standard exit status code ``130`` for keyboard interrupts.
|
||||
|
||||
|
||||
`0.9.6`_ (2016-08-13)
|
||||
---------------------
|
||||
|
||||
* Added Python 3 as a dependency for Homebrew installations
|
||||
to ensure some of the newer HTTP features work out of the box
|
||||
for macOS users (starting with HTTPie 0.9.4.).
|
||||
* Added the ability to unset a request header with ``Header:``, and send an
|
||||
empty value with ``Header;``.
|
||||
* Added ``--default-scheme <URL_SCHEME>`` to enable things like
|
||||
``$ alias https='http --default-scheme=https``.
|
||||
* Added ``-I`` as a shortcut for ``--ignore-stdin``.
|
||||
* Added fish shell completion (located in ``extras/httpie-completion.fish``
|
||||
in the Github repo).
|
||||
* Updated ``requests`` to 2.10.0 so that SOCKS support can be added via
|
||||
``pip install requests[socks]``.
|
||||
* Changed the default JSON ``Accept`` header from ``application/json``
|
||||
to ``application/json, */*``.
|
||||
* Changed the pre-processing of request HTTP headers so that any leading
|
||||
and trailing whitespace is removed.
|
||||
|
||||
|
||||
`0.9.4`_ (2016-07-01)
|
||||
---------------------
|
||||
|
||||
* Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests
|
||||
* Added ``--ssl=<PROTOCOL>`` to specify the desired SSL/TLS protocol version
|
||||
to use for HTTPS requests.
|
||||
* Added JSON detection with ``--json, -j`` to work around incorrect
|
||||
``Content-Type``
|
||||
* Added ``--all`` to show intermediate responses such as redirects (with ``--follow``)
|
||||
* Added ``--history-print, -P WHAT`` to specify formatting of intermediate responses
|
||||
* Added ``--max-redirects=N`` (default 30)
|
||||
* Added ``-A`` as short name for ``--auth-type``
|
||||
* Added ``-F`` as short name for ``--follow``
|
||||
* Removed the ``implicit_content_type`` config option
|
||||
(use ``"default_options": ["--form"]`` instead)
|
||||
* Redirected ``stdout`` doesn't trigger an error anymore when ``--output FILE``
|
||||
is set
|
||||
* Changed the default ``--style`` back to ``solarized`` for better support
|
||||
of light and dark terminals
|
||||
* Improved ``--debug`` output
|
||||
* Fixed ``--session`` when used with ``--download``
|
||||
* Fixed ``--download`` to trim too long filenames before saving the file
|
||||
* Fixed the handling of ``Content-Type`` with multiple ``+subtype`` parts
|
||||
* Removed the XML formatter as the implementation suffered from multiple issues
|
||||
|
||||
|
||||
|
||||
`0.9.3`_ (2016-01-01)
|
||||
---------------------
|
||||
|
||||
* Changed the default color ``--style`` from ``solarized`` to ``monokai``
|
||||
* Added basic Bash autocomplete support (need to be installed manually)
|
||||
* Added request details to connection error messages
|
||||
* Fixed ``'requests.packages.urllib3' has no attribute 'disable_warnings'``
|
||||
errors that occurred in some installations
|
||||
* Fixed colors and formatting on Windows
|
||||
* Fixed ``--auth`` prompt on Windows
|
||||
|
||||
|
||||
`0.9.2`_ (2015-02-24)
|
||||
---------------------
|
||||
|
||||
* Fixed compatibility with Requests 2.5.1
|
||||
* Changed the default JSON ``Content-Type`` to ``application/json`` as UTF-8
|
||||
is the default JSON encoding
|
||||
|
||||
|
||||
`0.9.1`_ (2015-02-07)
|
||||
---------------------
|
||||
|
||||
* Added support for Requests transport adapter plugins
|
||||
(see `httpie-unixsocket <https://github.com/httpie/httpie-unixsocket>`_
|
||||
and `httpie-http2 <https://github.com/httpie/httpie-http2>`_)
|
||||
|
||||
|
||||
`0.9.0`_ (2015-01-31)
|
||||
---------------------
|
||||
|
||||
* Added ``--cert`` and ``--cert-key`` parameters to specify a client side
|
||||
certificate and private key for SSL
|
||||
* Improved unicode support
|
||||
* Improved terminal color depth detection via ``curses``
|
||||
* To make it easier to deal with Windows paths in request items, ``\``
|
||||
now only escapes special characters (the ones that are used as key-value
|
||||
separators by HTTPie)
|
||||
* Switched from ``unittest`` to ``pytest``
|
||||
* Added Python `wheel` support
|
||||
* Various test suite improvements
|
||||
* Added ``CONTRIBUTING``
|
||||
* Fixed ``User-Agent`` overwriting when used within a session
|
||||
* Fixed handling of empty passwords in URL credentials
|
||||
* Fixed multiple file uploads with the same form field name
|
||||
* Fixed ``--output=/dev/null`` on Linux
|
||||
* Miscellaneous bugfixes
|
||||
|
||||
|
||||
`0.8.0`_ (2014-01-25)
|
||||
---------------------
|
||||
|
||||
* Added ``field=@file.txt`` and ``field:=@file.json`` for embedding
|
||||
the contents of text and JSON files into request data
|
||||
* Added curl-style shorthand for localhost
|
||||
* Fixed request ``Host`` header value output so that it doesn't contain
|
||||
credentials, if included in the URL
|
||||
|
||||
|
||||
`0.7.1`_ (2013-09-24)
|
||||
---------------------
|
||||
|
||||
* Added ``--ignore-stdin``
|
||||
* Added support for auth plugins
|
||||
* Improved ``--help`` output
|
||||
* Improved ``Content-Disposition`` parsing for ``--download`` mode
|
||||
* Update to Requests 2.0.0
|
||||
|
||||
|
||||
`0.6.0`_ (2013-06-03)
|
||||
---------------------
|
||||
|
||||
* XML data is now formatted
|
||||
* ``--session`` and ``--session-read-only`` now also accept paths to
|
||||
session files (eg. ``http --session=/tmp/session.json example.org``)
|
||||
|
||||
|
||||
`0.5.1`_ (2013-05-13)
|
||||
---------------------
|
||||
|
||||
* ``Content-*`` and ``If-*`` request headers are not stored in sessions
|
||||
anymore as they are request-specific
|
||||
|
||||
|
||||
`0.5.0`_ (2013-04-27)
|
||||
---------------------
|
||||
|
||||
* Added a download mode via ``--download``
|
||||
* Fixes miscellaneous bugs
|
||||
|
||||
|
||||
`0.4.1`_ (2013-02-26)
|
||||
---------------------
|
||||
|
||||
* Fixed ``setup.py``
|
||||
|
||||
|
||||
`0.4.0`_ (2013-02-22)
|
||||
---------------------
|
||||
|
||||
* Added Python 3.3 compatibility
|
||||
* Added Requests >= v1.0.4 compatibility
|
||||
* Added support for credentials in URL
|
||||
* Added ``--no-option`` for every ``--option`` to be config-friendly
|
||||
* Mutually exclusive arguments can be specified multiple times. The
|
||||
last value is used
|
||||
|
||||
|
||||
`0.3.0`_ (2012-09-21)
|
||||
---------------------
|
||||
|
||||
* Allow output redirection on Windows
|
||||
* Added configuration file
|
||||
* Added persistent session support
|
||||
* Renamed ``--allow-redirects`` to ``--follow``
|
||||
* Improved the usability of ``http --help``
|
||||
* Fixed installation on Windows with Python 3
|
||||
* Fixed colorized output on Windows with Python 3
|
||||
* CRLF HTTP header field separation in the output
|
||||
* Added exit status code ``2`` for timed-out requests
|
||||
* Added the option to separate colorizing and formatting
|
||||
(``--pretty=all``, ``--pretty=colors`` and ``--pretty=format``)
|
||||
``--ugly`` has bee removed in favor of ``--pretty=none``
|
||||
|
||||
|
||||
`0.2.7`_ (2012-08-07)
|
||||
---------------------
|
||||
|
||||
* Added compatibility with Requests 0.13.6
|
||||
* Added streamed terminal output. ``--stream, -S`` can be used to enable
|
||||
streaming also with ``--pretty`` and to ensure a more frequent output
|
||||
flushing
|
||||
* Added support for efficient large file downloads
|
||||
* Sort headers by name (unless ``--pretty=none``)
|
||||
* Response body is fetched only when needed (e.g., not with ``--headers``)
|
||||
* Improved content type matching
|
||||
* Updated Solarized color scheme
|
||||
* Windows: Added ``--output FILE`` to store output into a file
|
||||
(piping results in corrupted data on Windows)
|
||||
* Proper handling of binary requests and responses
|
||||
* Fixed printing of ``multipart/form-data`` requests
|
||||
* Renamed ``--traceback`` to ``--debug``
|
||||
|
||||
|
||||
`0.2.6`_ (2012-07-26)
|
||||
---------------------
|
||||
|
||||
* The short option for ``--headers`` is now ``-h`` (``-t`` has been
|
||||
removed, for usage use ``--help``)
|
||||
* Form data and URL parameters can have multiple fields with the same name
|
||||
(e.g.,``http -f url a=1 a=2``)
|
||||
* Added ``--check-status`` to exit with an error on HTTP 3xx, 4xx and
|
||||
5xx (3, 4, and 5, respectively)
|
||||
* If the output is piped to another program or redirected to a file,
|
||||
the default behaviour is to only print the response body
|
||||
(It can still be overwritten via the ``--print`` flag.)
|
||||
* Improved highlighting of HTTP headers
|
||||
* Added query string parameters (``param==value``)
|
||||
* Added support for terminal colors under Windows
|
||||
|
||||
|
||||
`0.2.5`_ (2012-07-17)
|
||||
---------------------
|
||||
|
||||
* Unicode characters in prettified JSON now don't get escaped for
|
||||
improved readability
|
||||
* --auth now prompts for a password if only a username provided
|
||||
* Added support for request payloads from a file path with automatic
|
||||
``Content-Type`` (``http URL @/path``)
|
||||
* Fixed missing query string when displaying the request headers via
|
||||
``--verbose``
|
||||
* Fixed Content-Type for requests with no data
|
||||
|
||||
|
||||
`0.2.2`_ (2012-06-24)
|
||||
---------------------
|
||||
|
||||
* The ``METHOD`` positional argument can now be omitted (defaults to
|
||||
``GET``, or to ``POST`` with data)
|
||||
* Fixed --verbose --form
|
||||
* Added support for Tox
|
||||
|
||||
|
||||
`0.2.1`_ (2012-06-13)
|
||||
---------------------
|
||||
|
||||
* Added compatibility with ``requests-0.12.1``
|
||||
* Dropped custom JSON and HTTP lexers in favor of the ones newly included
|
||||
in ``pygments-1.5``
|
||||
|
||||
|
||||
`0.2.0`_ (2012-04-25)
|
||||
---------------------
|
||||
|
||||
* Added Python 3 support
|
||||
* Added the ability to print the HTTP request as well as the response
|
||||
(see ``--print`` and ``--verbose``)
|
||||
* Added support for Digest authentication
|
||||
* Added file upload support
|
||||
(``http -f POST file_field_name@/path/to/file``)
|
||||
* Improved syntax highlighting for JSON
|
||||
* Added support for field name escaping
|
||||
* Many bug fixes
|
||||
|
||||
|
||||
`0.1.6`_ (2012-03-04)
|
||||
---------------------
|
||||
|
||||
* Fixed ``setup.py``
|
||||
|
||||
|
||||
`0.1.5`_ (2012-03-04)
|
||||
---------------------
|
||||
|
||||
* Many improvements and bug fixes
|
||||
|
||||
|
||||
`0.1.4`_ (2012-02-28)
|
||||
---------------------
|
||||
|
||||
* Many improvements and bug fixes
|
||||
|
||||
|
||||
`0.1.0`_ (2012-02-25)
|
||||
---------------------
|
||||
|
||||
* Initial public release
|
||||
|
||||
|
||||
.. _`0.1.0`: https://github.com/jakubroztocil/httpie/commit/b966efa
|
||||
.. _0.1.4: https://github.com/jakubroztocil/httpie/compare/b966efa...0.1.4
|
||||
.. _0.1.5: https://github.com/jakubroztocil/httpie/compare/0.1.4...0.1.5
|
||||
.. _0.1.6: https://github.com/jakubroztocil/httpie/compare/0.1.5...0.1.6
|
||||
.. _0.2.0: https://github.com/jakubroztocil/httpie/compare/0.1.6...0.2.0
|
||||
.. _0.2.1: https://github.com/jakubroztocil/httpie/compare/0.2.0...0.2.1
|
||||
.. _0.2.2: https://github.com/jakubroztocil/httpie/compare/0.2.1...0.2.2
|
||||
.. _0.2.5: https://github.com/jakubroztocil/httpie/compare/0.2.2...0.2.5
|
||||
.. _0.2.6: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.6
|
||||
.. _0.2.7: https://github.com/jakubroztocil/httpie/compare/0.2.5...0.2.7
|
||||
.. _0.3.0: https://github.com/jakubroztocil/httpie/compare/0.2.7...0.3.0
|
||||
.. _0.4.0: https://github.com/jakubroztocil/httpie/compare/0.3.0...0.4.0
|
||||
.. _0.4.1: https://github.com/jakubroztocil/httpie/compare/0.4.0...0.4.1
|
||||
.. _0.5.0: https://github.com/jakubroztocil/httpie/compare/0.4.1...0.5.0
|
||||
.. _0.5.1: https://github.com/jakubroztocil/httpie/compare/0.5.0...0.5.1
|
||||
.. _0.6.0: https://github.com/jakubroztocil/httpie/compare/0.5.1...0.6.0
|
||||
.. _0.7.1: https://github.com/jakubroztocil/httpie/compare/0.6.0...0.7.1
|
||||
.. _0.8.0: https://github.com/jakubroztocil/httpie/compare/0.7.1...0.8.0
|
||||
.. _0.9.0: https://github.com/jakubroztocil/httpie/compare/0.8.0...0.9.0
|
||||
.. _0.9.1: https://github.com/jakubroztocil/httpie/compare/0.9.0...0.9.1
|
||||
.. _0.9.2: https://github.com/jakubroztocil/httpie/compare/0.9.1...0.9.2
|
||||
.. _0.9.3: https://github.com/jakubroztocil/httpie/compare/0.9.2...0.9.3
|
||||
.. _0.9.4: https://github.com/jakubroztocil/httpie/compare/0.9.3...0.9.4
|
||||
.. _0.9.6: https://github.com/jakubroztocil/httpie/compare/0.9.4...0.9.6
|
||||
.. _0.9.8: https://github.com/jakubroztocil/httpie/compare/0.9.6...0.9.8
|
||||
.. _0.9.9: https://github.com/jakubroztocil/httpie/compare/0.9.8...0.9.9
|
||||
.. _1.0.0: https://github.com/jakubroztocil/httpie/compare/0.9.9...1.0.0
|
||||
.. _1.0.1: https://github.com/jakubroztocil/httpie/compare/1.0.0...1.0.1
|
||||
.. _1.0.2: https://github.com/jakubroztocil/httpie/compare/1.0.1...1.0.2
|
||||
.. _1.0.3-dev: https://github.com/jakubroztocil/httpie/compare/1.0.2...master
|
74
CODE_OF_CONDUCT.md
Normal file
74
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at jakub@roztocil.co. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||
version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
<https://www.contributor-covenant.org/faq>
|
196
CONTRIBUTING.md
Normal file
196
CONTRIBUTING.md
Normal file
@ -0,0 +1,196 @@
|
||||
# Contributing to HTTPie
|
||||
|
||||
Bug reports and code and documentation patches are welcome. You can
|
||||
help this project also by using the development version of HTTPie
|
||||
and by reporting any bugs you might encounter.
|
||||
|
||||
## 1. Reporting bugs
|
||||
|
||||
**It's important that you provide the full command argument list
|
||||
as well as the output of the failing command.**
|
||||
|
||||
Use the `--debug` flag and copy&paste both the command and its output
|
||||
to your bug report, e.g.:
|
||||
|
||||
```bash
|
||||
$ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
|
||||
<COMPLETE OUTPUT>
|
||||
```
|
||||
|
||||
## 2. Contributing Code and Docs
|
||||
|
||||
Before working on a new feature or a bug, please browse [existing issues](https://github.com/httpie/httpie/issues)
|
||||
to see whether it has previously been discussed.
|
||||
|
||||
If your change alters HTTPie’s behaviour or interface, it's a good idea to
|
||||
discuss it before you start working on it.
|
||||
|
||||
If you are fixing an issue, the first step should be to create a test case that
|
||||
reproduces the incorrect behaviour. That will also help you to build an
|
||||
understanding of the issue at hand.
|
||||
|
||||
**Pull requests introducing code changes without tests
|
||||
will generally not get merged. The same goes for PRs changing HTTPie’s
|
||||
behaviour and not providing documentation.**
|
||||
|
||||
Conversely, PRs consisting of documentation improvements or tests
|
||||
for existing-yet-previously-untested behavior will very likely be merged.
|
||||
Therefore, docs and tests improvements are a great candidate for your first
|
||||
contribution.
|
||||
|
||||
Consider also adding a [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md) entry for your changes.
|
||||
|
||||
### Development Environment
|
||||
|
||||
#### Getting the code
|
||||
|
||||
Go to <https://github.com/httpie/httpie> and fork the project repository.
|
||||
|
||||
```bash
|
||||
# Clone your fork
|
||||
$ git clone git@github.com:<YOU>/httpie.git
|
||||
|
||||
# Enter the project directory
|
||||
$ cd httpie
|
||||
|
||||
# Create a branch for your changes
|
||||
$ git checkout -b my_topical_branch
|
||||
```
|
||||
|
||||
#### Setup
|
||||
|
||||
The [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) contains a bunch of tasks to get you started. Just run
|
||||
the following command, which:
|
||||
|
||||
- Creates an isolated Python virtual environment inside `./venv`
|
||||
(via the standard library [venv](https://docs.python.org/3/library/venv.html) tool);
|
||||
- installs all dependencies and also installs HTTPie
|
||||
(in editable mode so that the `http` command will point to your
|
||||
working copy).
|
||||
- and runs tests (It is the same as running `make install test`).
|
||||
|
||||
```bash
|
||||
$ make
|
||||
```
|
||||
|
||||
#### Python virtual environment
|
||||
|
||||
Activate the Python virtual environment—created via the `make install`
|
||||
task during [setup](#setup) for your active shell session using the following command:
|
||||
|
||||
```bash
|
||||
$ source venv/bin/activate
|
||||
```
|
||||
|
||||
(If you use `virtualenvwrapper`, you can also use `workon httpie` to
|
||||
activate the environment — we have created a symlink for you. It’s a bit of
|
||||
a hack but it works™.)
|
||||
|
||||
You should now see `(httpie)` next to your shell prompt, and
|
||||
the `http` command should point to your development copy:
|
||||
|
||||
```bash
|
||||
(httpie) ~/Code/httpie $ which http
|
||||
/Users/<user>/Code/httpie/venv/bin/http
|
||||
(httpie) ~/Code/httpie $ http --version
|
||||
2.0.0-dev
|
||||
```
|
||||
|
||||
(Btw, you don’t need to activate the virtual environment if you just want
|
||||
run some of the `make` tasks. You can also invoke the development
|
||||
version of HTTPie directly with `./venv/bin/http` without having to activate
|
||||
the environment first. The same goes for `./venv/bin/pytest`, etc.).
|
||||
|
||||
### Making Changes
|
||||
|
||||
Please make sure your changes conform to [Style Guide for Python Code](https://python.org/dev/peps/pep-0008/) (PEP8)
|
||||
and that `make pycodestyle` passes.
|
||||
|
||||
### Testing & CI
|
||||
|
||||
Please add tests for any new features and bug fixes.
|
||||
|
||||
When you open a Pull Request, [GitHub Actions](https://github.com/httpie/httpie/actions) will automatically run HTTPie’s [test suite](https://github.com/httpie/httpie/tree/master/tests) against your code, so please make sure all checks pass.
|
||||
|
||||
#### Running tests locally
|
||||
|
||||
HTTPie uses the [pytest](https://pytest.org/) runner.
|
||||
|
||||
```bash
|
||||
# Run tests on the current Python interpreter with coverage.
|
||||
$ make test
|
||||
|
||||
# Run tests with coverage
|
||||
$ make test-cover
|
||||
|
||||
# Test PEP8 compliance
|
||||
$ make codestyle
|
||||
|
||||
# Run extended tests — for code as well as .md files syntax, packaging, etc.
|
||||
$ make test-all
|
||||
```
|
||||
|
||||
#### Running specific tests
|
||||
|
||||
After you have activated your virtual environment (see [setup](#setup)), you
|
||||
can run specific tests from the terminal:
|
||||
|
||||
```bash
|
||||
# Run specific tests on the current Python
|
||||
$ python -m pytest tests/test_uploads.py
|
||||
$ python -m pytest tests/test_uploads.py::TestMultipartFormDataFileUpload
|
||||
$ python -m pytest tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
|
||||
```
|
||||
|
||||
See [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) for additional development utilities.
|
||||
|
||||
#### Windows
|
||||
|
||||
If you are on a Windows machine and not able to run `make`,
|
||||
follow the next steps for a basic setup. As a prerequisite, you need to have
|
||||
Python 3.6+ installed.
|
||||
|
||||
Create a virtual environment and activate it:
|
||||
|
||||
```powershell
|
||||
C:\> python -m venv --prompt httpie venv
|
||||
C:\> venv\Scripts\activate
|
||||
```
|
||||
|
||||
Install HTTPie in editable mode with all the dependencies:
|
||||
|
||||
```powershell
|
||||
C:\> python -m pip install --upgrade -e . -r requirements-dev.txt
|
||||
```
|
||||
|
||||
You should now see `(httpie)` next to your shell prompt, and
|
||||
the `http` command should point to your development copy:
|
||||
|
||||
```powershell
|
||||
# In PowerShell:
|
||||
(httpie) PS C:\Users\ovezovs\httpie> Get-Command http
|
||||
CommandType Name Version Source
|
||||
----------- ---- ------- ------
|
||||
Application http.exe 0.0.0.0 C:\Users\ovezovs\httpie\venv\Scripts\http.exe
|
||||
```
|
||||
|
||||
```bash
|
||||
# In CMD:
|
||||
(httpie) C:\Users\ovezovs\httpie> where http
|
||||
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
|
||||
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
|
||||
|
||||
(httpie) C:\Users\ovezovs\httpie> http --version
|
||||
2.3.0-dev
|
||||
```
|
||||
|
||||
Use `pytest` to run tests locally with an active virtual environment:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
$ python -m pytest
|
||||
```
|
||||
|
||||
______________________________________________________________________
|
||||
|
||||
Finally, feel free to add yourself to [AUTHORS](https://github.com/httpie/httpie/blob/master/AUTHORS.md)!
|
117
CONTRIBUTING.rst
117
CONTRIBUTING.rst
@ -1,117 +0,0 @@
|
||||
######################
|
||||
Contributing to HTTPie
|
||||
######################
|
||||
|
||||
Bug reports and code and documentation patches are welcome. You can
|
||||
help this project also by using the development version of HTTPie
|
||||
and by reporting any bugs you might encounter.
|
||||
|
||||
1. Reporting bugs
|
||||
=================
|
||||
|
||||
**It's important that you provide the full command argument list
|
||||
as well as the output of the failing command.**
|
||||
Use the ``--debug`` flag and copy&paste both the command and its output
|
||||
to your bug report, e.g.:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ http --debug [COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR]
|
||||
[COMPLETE OUTPUT]
|
||||
|
||||
|
||||
2. Contributing Code and Docs
|
||||
=============================
|
||||
|
||||
Before working on a new feature or a bug, please browse `existing issues`_
|
||||
to see whether it has been previously discussed. If the change in question
|
||||
is a bigger one, it's always good to discuss before you start working on
|
||||
it.
|
||||
|
||||
|
||||
Creating Development Environment
|
||||
--------------------------------
|
||||
|
||||
Go to https://github.com/jakubroztocil/httpie and fork the project repository.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/<YOU>/httpie
|
||||
|
||||
cd httpie
|
||||
|
||||
git checkout -b my_topical_branch
|
||||
|
||||
# (Recommended: create a new virtualenv)
|
||||
|
||||
# Install dev. requirements and also HTTPie (in editable mode
|
||||
# so that the `http' command will point to your working copy):
|
||||
make init
|
||||
|
||||
|
||||
Making Changes
|
||||
--------------
|
||||
|
||||
Please make sure your changes conform to `Style Guide for Python Code`_ (PEP8)
|
||||
and that ``make pycodestyle`` passes.
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
Before opening a pull requests, please make sure the `test suite`_ passes
|
||||
in all of the `supported Python environments`_. You should also add tests
|
||||
for any new features and bug fixes.
|
||||
|
||||
HTTPie uses `pytest`_ and `Tox`_ for testing.
|
||||
|
||||
|
||||
Running all tests:
|
||||
******************
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run all tests on the current Python interpreter with coverage
|
||||
make test
|
||||
|
||||
# Run all tests in all of the supported and available Pythons via Tox
|
||||
make test-tox
|
||||
|
||||
# Run all tests for code as well as packaging, etc.
|
||||
make test-all
|
||||
|
||||
# Test PEP8 compliance
|
||||
make pycodestyle
|
||||
|
||||
|
||||
Running specific tests:
|
||||
***********************
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run specific tests on the current Python
|
||||
py.test tests/test_uploads.py
|
||||
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload
|
||||
py.test tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok
|
||||
|
||||
# Run specific tests on the on all Pythons via Tox
|
||||
# (change to `tox -e py37' to limit Python version)
|
||||
tox -- tests/test_uploads.py --verbose
|
||||
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload --verbose
|
||||
tox -- tests/test_uploads.py::TestMultipartFormDataFileUpload::test_upload_ok --verbose
|
||||
|
||||
-----
|
||||
|
||||
See `Makefile`_ for additional development utilities.
|
||||
Don't forget to add yourself to `AUTHORS`_!
|
||||
|
||||
|
||||
.. _Tox: http://tox.testrun.org
|
||||
.. _supported Python environments: https://github.com/jakubroztocil/httpie/blob/master/tox.ini
|
||||
.. _existing issues: https://github.com/jakubroztocil/httpie/issues?state=open
|
||||
.. _AUTHORS: https://github.com/jakubroztocil/httpie/blob/master/AUTHORS.rst
|
||||
.. _Makefile: https://github.com/jakubroztocil/httpie/blob/master/Makefile
|
||||
.. _pytest: http://pytest.org/
|
||||
.. _Style Guide for Python Code: http://python.org/dev/peps/pep-0008/
|
||||
.. _test suite: https://github.com/jakubroztocil/httpie/tree/master/tests
|
10
LICENSE
10
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright © 2012-2017 Jakub Roztocil <jakub@roztocil.co>
|
||||
Copyright © 2012-2021 Jakub Roztocil <jakub@roztocil.co>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
@ -10,14 +10,14 @@ modification, are permitted provided that the following conditions are met:
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of The author nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR AND CONTRIBUTORS BE LIABLE FOR
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
|
10
MANIFEST.in
10
MANIFEST.in
@ -1,4 +1,8 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include CHANGELOG.rst
|
||||
include AUTHORS.rst
|
||||
include README.md
|
||||
include CHANGELOG.md
|
||||
include AUTHORS.md
|
||||
include docs/README.md
|
||||
|
||||
# <https://github.com/httpie/httpie/issues/182>
|
||||
recursive-include tests/ *
|
||||
|
200
Makefile
200
Makefile
@ -1,45 +1,100 @@
|
||||
###############################################################################
|
||||
# See ./CONTRIBUTING.rst
|
||||
# See ./CONTRIBUTING.md
|
||||
###############################################################################
|
||||
|
||||
.PHONY: build
|
||||
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
VERSION=$(shell grep __version__ httpie/__init__.py)
|
||||
REQUIREMENTS="requirements-dev.txt"
|
||||
TAG="\n\n\033[0;32m\#\#\# "
|
||||
END=" \#\#\# \033[0m\n"
|
||||
H1="\n\n\033[0;32m\#\#\# "
|
||||
H1END=" \#\#\# \033[0m\n"
|
||||
|
||||
|
||||
all: test
|
||||
# Only used to create our venv.
|
||||
SYSTEM_PYTHON=python3
|
||||
|
||||
VENV_ROOT=venv
|
||||
VENV_BIN=$(VENV_ROOT)/bin
|
||||
VENV_PIP=$(VENV_BIN)/pip3
|
||||
VENV_PYTHON=$(VENV_BIN)/python
|
||||
|
||||
|
||||
init: uninstall-httpie
|
||||
@echo $(TAG)Installing dev requirements$(END)
|
||||
pip install --upgrade -r $(REQUIREMENTS)
|
||||
export PATH := $(VENV_BIN):$(PATH)
|
||||
|
||||
@echo $(TAG)Installing HTTPie$(END)
|
||||
pip install --upgrade --editable .
|
||||
|
||||
all: uninstall-httpie install test
|
||||
|
||||
|
||||
install: venv install-reqs
|
||||
|
||||
|
||||
install-reqs:
|
||||
@echo $(H1)Updating package tools$(H1END)
|
||||
$(VENV_PIP) install --upgrade pip wheel
|
||||
|
||||
@echo $(H1)Installing dev requirements$(H1END)
|
||||
$(VENV_PIP) install --upgrade --editable '.[dev]'
|
||||
|
||||
@echo $(H1)Installing HTTPie$(H1END)
|
||||
$(VENV_PIP) install --upgrade --editable .
|
||||
|
||||
@echo
|
||||
|
||||
|
||||
clean:
|
||||
@echo $(TAG)Cleaning up$(END)
|
||||
rm -rf .tox *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
|
||||
find . -name '__pycache__' -delete -print -o -name '*.pyc' -delete -print
|
||||
@echo $(H1)Cleaning up$(H1END)
|
||||
rm -rf $(VENV_ROOT)
|
||||
# Remove symlink for virtualenvwrapper, if we’ve created one.
|
||||
[ -n "$(WORKON_HOME)" -a -L "$(WORKON_HOME)/httpie" -a -f "$(WORKON_HOME)/httpie" ] && rm $(WORKON_HOME)/httpie || true
|
||||
rm -rf *.egg dist build .coverage .cache .pytest_cache httpie.egg-info
|
||||
find . -name '__pycache__' -delete -o -name '*.pyc' -delete
|
||||
@echo
|
||||
|
||||
|
||||
venv:
|
||||
@echo $(H1)Creating a Python environment $(VENV_ROOT) $(H1END)
|
||||
|
||||
$(SYSTEM_PYTHON) -m venv --prompt httpie $(VENV_ROOT)
|
||||
|
||||
@echo
|
||||
@echo done.
|
||||
@echo
|
||||
@echo To active it manually, run:
|
||||
@echo
|
||||
@echo " source $(VENV_BIN)/activate"
|
||||
@echo
|
||||
@echo '(learn more: https://docs.python.org/3/library/venv.html)'
|
||||
@echo
|
||||
@if [ -n "$(WORKON_HOME)" ]; then \
|
||||
echo $(ROOT_DIR) > $(VENV_ROOT)/.project; \
|
||||
if [ ! -d $(WORKON_HOME)/httpie -a ! -L $(WORKON_HOME)/httpie ]; then \
|
||||
ln -s $(ROOT_DIR)/$(VENV_ROOT) $(WORKON_HOME)/httpie ; \
|
||||
echo ''; \
|
||||
echo 'Since you use virtualenvwrapper, we created a symlink'; \
|
||||
echo 'so you can also use "workon httpie" to activate the venv.'; \
|
||||
echo ''; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Testing
|
||||
###############################################################################
|
||||
|
||||
|
||||
test: init
|
||||
@echo $(TAG)Running tests on the current Python interpreter with coverage $(END)
|
||||
py.test --cov ./httpie --cov ./tests --doctest-modules --verbose ./httpie ./tests
|
||||
test:
|
||||
@echo $(H1)Running tests$(HEADER_EXTRA)$(H1END)
|
||||
$(VENV_BIN)/python -m pytest $(COV)
|
||||
@echo
|
||||
|
||||
|
||||
test-cover: COV=--cov=httpie --cov=tests
|
||||
test-cover: HEADER_EXTRA=' (with coverage)'
|
||||
test-cover: test
|
||||
|
||||
|
||||
# test-all is meant to test everything — even this Makefile
|
||||
test-all: uninstall-all clean init test test-tox test-dist pycodestyle
|
||||
test-all: clean install test test-dist codestyle
|
||||
@echo
|
||||
|
||||
|
||||
@ -47,38 +102,54 @@ test-dist: test-sdist test-bdist-wheel
|
||||
@echo
|
||||
|
||||
|
||||
test-tox: init
|
||||
@echo $(TAG)Running tests on all Pythons via Tox$(END)
|
||||
tox
|
||||
test-sdist: clean venv
|
||||
@echo $(H1)Testing sdist build an installation$(H1END)
|
||||
$(VENV_PYTHON) setup.py sdist
|
||||
$(VENV_PIP) install --force-reinstall --upgrade dist/*.gz
|
||||
$(VENV_BIN)/http --version
|
||||
@echo
|
||||
|
||||
|
||||
test-sdist: clean uninstall-httpie
|
||||
@echo $(TAG)Testing sdist build an installation$(END)
|
||||
python setup.py sdist
|
||||
pip install --force-reinstall --upgrade dist/*.gz
|
||||
which http
|
||||
test-bdist-wheel: clean venv
|
||||
@echo $(H1)Testing wheel build an installation$(H1END)
|
||||
$(VENV_PIP) install wheel
|
||||
$(VENV_PYTHON) setup.py bdist_wheel
|
||||
$(VENV_PIP) install --force-reinstall --upgrade dist/*.whl
|
||||
$(VENV_BIN)/http --version
|
||||
@echo
|
||||
|
||||
|
||||
test-bdist-wheel: clean uninstall-httpie
|
||||
@echo $(TAG)Testing wheel build an installation$(END)
|
||||
python setup.py bdist_wheel
|
||||
pip install --force-reinstall --upgrade dist/*.whl
|
||||
which http
|
||||
twine-check:
|
||||
twine check dist/*
|
||||
|
||||
|
||||
# Kept for convenience, "make codestyle" is preferred though
|
||||
pycodestyle: codestyle
|
||||
|
||||
|
||||
codestyle:
|
||||
@echo $(H1)Running flake8$(H1END)
|
||||
@[ -f $(VENV_BIN)/flake8 ] || $(VENV_PIP) install --upgrade --editable '.[dev]'
|
||||
$(VENV_BIN)/flake8 httpie/ tests/ docs/packaging/brew/ *.py
|
||||
@echo
|
||||
|
||||
|
||||
pycodestyle:
|
||||
which pycodestyle || pip install pycodestyle
|
||||
pycodestyle
|
||||
codecov-upload:
|
||||
@echo $(H1)Running codecov$(H1END)
|
||||
@[ -f $(VENV_BIN)/codecov ] || $(VENV_PIP) install codecov
|
||||
# $(VENV_BIN)/codecov --required
|
||||
$(VENV_BIN)/codecov
|
||||
@echo
|
||||
|
||||
|
||||
coveralls:
|
||||
which coveralls || pip install python-coveralls
|
||||
coveralls
|
||||
@echo
|
||||
doc-check:
|
||||
@echo $(H1)Running documentations checks$(H1END)
|
||||
mdl --git-recurse --style docs/markdownlint.rb .
|
||||
|
||||
|
||||
doc-update-install:
|
||||
@echo $(H1)Updating installation instructions in the docs$(H1END)
|
||||
$(VENV_PYTHON) docs/installation/generate.py
|
||||
|
||||
|
||||
###############################################################################
|
||||
@ -86,16 +157,21 @@ coveralls:
|
||||
###############################################################################
|
||||
|
||||
|
||||
build:
|
||||
rm -rf build/
|
||||
$(VENV_PYTHON) setup.py sdist bdist_wheel
|
||||
|
||||
|
||||
publish: test-all publish-no-test
|
||||
|
||||
|
||||
publish-no-test:
|
||||
@echo $(TAG)Testing wheel build an installation$(END)
|
||||
@echo $(H1)Testing wheel build an installation$(H1END)
|
||||
@echo "$(VERSION)"
|
||||
@echo "$(VERSION)" | grep -q "dev" && echo '!!!Not publishing dev version!!!' && exit 1 || echo ok
|
||||
python setup.py register
|
||||
python setup.py sdist upload
|
||||
python setup.py bdist_wheel upload
|
||||
make build
|
||||
make twine-check
|
||||
$(VENV_BIN)/twine upload --repository=httpie dist/*
|
||||
@echo
|
||||
|
||||
|
||||
@ -105,44 +181,32 @@ publish-no-test:
|
||||
###############################################################################
|
||||
|
||||
uninstall-httpie:
|
||||
@echo $(TAG)Uninstalling httpie$(END)
|
||||
- pip uninstall --yes httpie &2>/dev/null
|
||||
@echo $(H1)Uninstalling httpie$(H1END)
|
||||
- $(VENV_PIP) uninstall --yes httpie &2>/dev/null
|
||||
|
||||
@echo "Verifying…"
|
||||
cd .. && ! python -m httpie --version &2>/dev/null
|
||||
cd .. && ! $(VENV_PYTHON) -m httpie --version &2>/dev/null
|
||||
|
||||
@echo "Done"
|
||||
@echo
|
||||
|
||||
|
||||
uninstall-all: uninstall-httpie
|
||||
|
||||
@echo $(TAG)Uninstalling httpie requirements$(END)
|
||||
- pip uninstall --yes pygments requests
|
||||
|
||||
@echo $(TAG)Uninstalling development requirements$(END)
|
||||
- pip uninstall --yes -r $(REQUIREMENTS)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Docs
|
||||
# Homebrew
|
||||
###############################################################################
|
||||
|
||||
pdf:
|
||||
# NOTE: rst2pdf needs to be installed manually and against a Python 2
|
||||
@echo "Converting README.rst to PDF…"
|
||||
rst2pdf \
|
||||
--strip-elements-with-class=no-pdf \
|
||||
README.rst \
|
||||
-o README.pdf
|
||||
@echo "Done"
|
||||
@echo
|
||||
brew-deps:
|
||||
docs/packaging/brew/brew-deps.py
|
||||
|
||||
brew-test:
|
||||
@echo $(H1)Uninstalling httpie$(H1END)
|
||||
- brew uninstall httpie
|
||||
|
||||
###############################################################################
|
||||
# Utils
|
||||
###############################################################################
|
||||
@echo $(H1)Building from source…$(H1END)
|
||||
- brew install --build-from-source ./docs/packaging/brew/httpie.rb
|
||||
|
||||
@echo $(H1)Verifying…$(H1END)
|
||||
brew test httpie
|
||||
|
||||
homebrew-formula-vars:
|
||||
extras/get-homebrew-formula-vars.py
|
||||
@echo $(H1)Auditing…$(H1END)
|
||||
brew audit --strict httpie
|
||||
|
84
README.md
Normal file
84
README.md
Normal file
@ -0,0 +1,84 @@
|
||||
<br/>
|
||||
<a href="https://httpie.io" target="blank_">
|
||||
<img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" />
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
# HTTPie: human-friendly CLI HTTP client for the API era
|
||||
|
||||
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.
|
||||
HTTPie is designed for testing, debugging, and generally interacting with APIs & HTTP servers.
|
||||
The `http` & `https` commands allow for creating and sending arbitrary HTTP requests.
|
||||
They use simple and natural syntax and provide formatted and colorized output.
|
||||
|
||||
[](https://httpie.org/docs)
|
||||
[](https://pypi.python.org/pypi/httpie)
|
||||
[](https://github.com/httpie/httpie/actions)
|
||||
[](https://codecov.io/gh/httpie/httpie)
|
||||
[](https://twitter.com/httpie)
|
||||
[](https://httpie.io/discord)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Installation instructions →](https://httpie.io/docs#installation)
|
||||
- [Full documentation →](https://httpie.io/docs)
|
||||
|
||||
## Features
|
||||
|
||||
- Expressive and intuitive syntax
|
||||
- Formatted and colorized terminal output
|
||||
- Built-in JSON support
|
||||
- Forms and file uploads
|
||||
- HTTPS, proxies, and authentication
|
||||
- Arbitrary request data
|
||||
- Custom headers
|
||||
- Persistent sessions
|
||||
- `wget`-like downloads
|
||||
|
||||
[See all features →](https://httpie.io/docs)
|
||||
|
||||
## Examples
|
||||
|
||||
Hello World:
|
||||
|
||||
```bash
|
||||
$ https httpie.io/hello
|
||||
```
|
||||
|
||||
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
|
||||
|
||||
```bash
|
||||
$ http PUT pie.dev/put X-API-Token:123 name=John
|
||||
```
|
||||
|
||||
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
|
||||
|
||||
```bash
|
||||
$ http --offline pie.dev/post hello=offline
|
||||
```
|
||||
|
||||
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
|
||||
|
||||
```bash
|
||||
$ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
|
||||
```
|
||||
|
||||
[See more examples →](https://httpie.io/docs#examples)
|
||||
|
||||
## Community & support
|
||||
|
||||
- Visit the [HTTPie website](https://httpie.io) for full documentation and useful links.
|
||||
- Join our [Discord server](https://httpie.io/discord) is to ask questions, discuss features, and for general API chat.
|
||||
- Tweet at [@httpie](https://twitter.com/httpie) on Twitter.
|
||||
- Use [StackOverflow](https://stackoverflow.com/questions/tagged/httpie) to ask questions and include a `httpie` tag.
|
||||
- Create [GitHub Issues](https://github.com/httpie/httpie/issues) for bug reports and feature requests.
|
||||
- Subscribe to the [HTTPie newsletter](https://httpie.io) for occasional updates.
|
||||
|
||||
## Contributing
|
||||
|
||||
Have a look through existing [Issues](https://github.com/httpie/httpie/issues) and [Pull Requests](https://github.com/httpie/httpie/pulls) that you could help with. If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/httpie/httpie/issues) using one of the templates provided.
|
||||
|
||||
[See contribution guide →](https://github.com/httpie/httpie/blob/master/CONTRIBUTING.md)
|
1727
README.rst
1727
README.rst
File diff suppressed because it is too large
Load Diff
1979
docs/README.md
Normal file
1979
docs/README.md
Normal file
File diff suppressed because it is too large
Load Diff
5
docs/config.json
Normal file
5
docs/config.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"website": {
|
||||
"master_and_released_docs_differ_after": "8f8851f1dbd511d3bc0ea0f6da7459045610afce"
|
||||
}
|
||||
}
|
BIN
docs/httpie-animation.gif
Normal file
BIN
docs/httpie-animation.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1019 KiB |
1
docs/httpie-logo.svg
Normal file
1
docs/httpie-logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1635.31 470"><defs><style>.cls-1{fill:#4b78e6;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M1322.19,73.91h0a36.56,36.56,0,0,1,36.56-36.29h3.41a36.56,36.56,0,0,1,36.56,36.83h0a36.56,36.56,0,0,1-36.56,36.29h-3.41A36.56,36.56,0,0,1,1322.19,73.91Zm6.16,276.93V142.35a7.94,7.94,0,0,1,8-7.94h48.32a7.93,7.93,0,0,1,7.94,7.94V350.84a7.94,7.94,0,0,1-7.94,7.94H1336.3A8,8,0,0,1,1328.35,350.84Z"/><path class="cls-1" d="M1635.31,233.34c0-61.06-33.28-105.09-101.71-105.09-72.17,0-114.82,45.45-114.82,123.08,0,74.79,46.86,113.6,113.89,113.6,56.83,0,85.93-27.17,98.33-63.86a8,8,0,0,0-5.34-10.28l-40.32-11.39a8,8,0,0,0-9.54,4.73c-5.77,14.37-16.57,25.42-42.2,25.42-29.32,0-46.06-13.62-50.74-44.29a7.17,7.17,0,0,0,.81.08h143.7a8,8,0,0,0,7.94-7.94V242.23c0-.09,0-.18,0-.28C1635.31,239.17,1635.31,236.36,1635.31,233.34Zm-103.58-51.6c28.59,0,43.12,15.15,45,45H1483C1487.21,195,1503.61,181.74,1531.73,181.74Z"/><path class="cls-1" d="M581.91,358.75H533.56a7.93,7.93,0,0,1-7.94-7.94V76.39a7.93,7.93,0,0,1,7.94-7.94h48.35a7.93,7.93,0,0,1,7.94,7.94v84.66a6,6,0,0,0,11.22,2.77c13.45-25.56,34.68-35.33,60.42-35.69,38.66-.55,70,31.45,70,70.12V350.81a7.94,7.94,0,0,1-7.94,7.94H675.63a7.94,7.94,0,0,1-7.94-7.94V227.1c0-23.21-10.32-40.73-37-40.73-25.79,0-40.8,15.15-40.8,40.73V350.81A7.93,7.93,0,0,1,581.91,358.75Z"/><path class="cls-1" d="M1052.84,306.12a7.94,7.94,0,0,0-9.77-6.78c-6.47,1.55-13.73,3.05-20.35,3.05-19.23,0-25.79-8.52-25.79-26.52V188.26h50.67a7.94,7.94,0,0,0,7.94-7.94v-38.1a8,8,0,0,0-7.94-7.95H996.93V85.86A7.94,7.94,0,0,0,989,77.92H941.1a7.93,7.93,0,0,0-7.94,7.94v48.41H842.67V85.86a7.94,7.94,0,0,0-7.94-7.94H786.84a7.93,7.93,0,0,0-7.94,7.94v48.41H761.05a7.94,7.94,0,0,0-7.94,7.95v38.1a7.93,7.93,0,0,0,7.94,7.94H778.9v99.93c0,42.62,21.57,77.19,73.15,77.19,21.16,0,32.43-2.5,46.08-6.56a8,8,0,0,0,5.65-8.56l-5.2-44.14a7.94,7.94,0,0,0-9.77-6.78c-6.47,1.55-13.73,3.05-20.35,3.05-19.23,0-25.79-8.52-25.79-26.52V188.26h90.49v99.93c0,42.62,21.57,77.19,73.14,77.19,21.17,0,32.44-2.5,46.09-6.56a8,8,0,0,0,5.65-8.56Z"/><path class="cls-1" d="M1219.14,365.27c-28.49,0-49.51-10.92-62.87-35.86a6,6,0,0,0-11.19,2.84v82.83a7.93,7.93,0,0,1-7.94,7.94h-48.32a7.94,7.94,0,0,1-8-7.94V142.21a8,8,0,0,1,8-7.94h48.32a7.94,7.94,0,0,1,7.94,7.94v18.95c0,6.13,8.21,8.3,11.15,2.92,13.74-25.16,35.63-36,64.31-36,53.43,0,81.08,44,81.08,116.92C1301.62,320.78,1273,365.27,1219.14,365.27Zm19.21-119.76c0-37.39-14.06-59.17-46.4-59.17-29.53,0-46.87,20.35-46.87,57.28v4.26c0,36.45,17.34,59.17,46.87,59.17C1223.82,307.05,1238.35,284.33,1238.35,245.51Z"/><path class="cls-1" d="M394.41,102.12C394,45.31,346.61,0,289.8,0H104.69C48.31,0,1.09,44.6,0,101A103.07,103.07,0,0,0,103,205.92h82.7a6,6,0,0,1,2.39,11.42L61.31,272.91A103.09,103.09,0,0,0,0,367.83C.43,424.65,47.79,470,104.62,470H148c57.23,0,104.88-45.9,104.79-103.13a103.1,103.1,0,0,0-64.49-95.31,5.94,5.94,0,0,1-.1-10.94l145-63.58A103.08,103.08,0,0,0,394.41,102.12Z"/></g></g></svg>
|
After Width: | Height: | Size: 2.9 KiB |
5
docs/installation/README.md
Normal file
5
docs/installation/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
Here we maintain a database of installation methods, from which we generate
|
||||
the installation section in docs. If you’d like add or update an installation method,
|
||||
edit [methods.yml](./methods.yml), do not edit the main docs directly.
|
||||
|
||||
For HTTPie installation instructions see: <https://httpie.io/docs#installation>.
|
85
docs/installation/generate.py
Normal file
85
docs/installation/generate.py
Normal file
@ -0,0 +1,85 @@
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
Database = Dict[str, dict]
|
||||
|
||||
# Files
|
||||
HERE = Path(__file__).parent
|
||||
DB_FILE = HERE / 'methods.yml'
|
||||
DOC_FILE = HERE.parent / 'README.md'
|
||||
TPL_FILE = HERE / 'installation.jinja2'
|
||||
|
||||
# Database keys
|
||||
KEY_DOC_STRUCTURE = 'docs-structure'
|
||||
KEY_TOOLS = 'tools'
|
||||
|
||||
# Markers in-between content will be put.
|
||||
MARKER_START = '<div data-installation-instructions>'
|
||||
MARKER_END = '</div>'
|
||||
|
||||
|
||||
def generate_documentation() -> str:
|
||||
database = load_database()
|
||||
structure = build_docs_structure(database)
|
||||
template = Template(source=TPL_FILE.read_text(encoding='utf-8'))
|
||||
output = template.render(structure=structure)
|
||||
output = clean_template_output(output)
|
||||
return output
|
||||
|
||||
|
||||
def save_doc_file(content: str) -> None:
|
||||
current_doc = load_doc_file()
|
||||
marker_start = current_doc.find(MARKER_START) + len(MARKER_START)
|
||||
assert marker_start > 0, 'cannot find the start marker'
|
||||
marker_end = current_doc.find(MARKER_END, marker_start)
|
||||
assert marker_start < marker_end, f'{marker_end=} < {marker_start=}'
|
||||
updated_doc = (
|
||||
current_doc[:marker_start]
|
||||
+ '\n\n'
|
||||
+ content
|
||||
+ '\n\n'
|
||||
+ current_doc[marker_end:]
|
||||
)
|
||||
if current_doc != updated_doc:
|
||||
DOC_FILE.write_text(updated_doc, encoding='utf-8')
|
||||
|
||||
|
||||
def build_docs_structure(database: Database):
|
||||
tools = database[KEY_TOOLS]
|
||||
assert len(tools) == len({tool['title'] for tool in tools.values()}), 'tool titles need to be unique'
|
||||
tree = database[KEY_DOC_STRUCTURE]
|
||||
structure = []
|
||||
for platform, tools_ids in tree.items():
|
||||
assert platform.isalnum(), f'{platform=} must be alpha-numeric for generated links to work'
|
||||
platform_tools = [tools[tool_id] for tool_id in tools_ids]
|
||||
structure.append((platform, platform_tools))
|
||||
return structure
|
||||
|
||||
|
||||
def clean_template_output(output):
|
||||
output = '\n'.join(line.strip() for line in output.strip().splitlines())
|
||||
output = re.sub('\n{3,}', '\n\n', output)
|
||||
return output
|
||||
|
||||
|
||||
def load_database() -> Database:
|
||||
return yaml.safe_load(DB_FILE.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def load_doc_file() -> str:
|
||||
return DOC_FILE.read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
content = generate_documentation()
|
||||
save_doc_file(content)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
37
docs/installation/installation.jinja2
Normal file
37
docs/installation/installation.jinja2
Normal file
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
THE INSTALLATION SECTION IS GENERATED
|
||||
|
||||
Do not edit here, but in docs/installation/.
|
||||
|
||||
-->
|
||||
{% for platform, tools in structure %}
|
||||
- [{{ platform }}](#{{ platform.lower() }}){% endfor %} {# <= keep `endfor` here to prevent unwanted `\n` #}
|
||||
|
||||
{% for platform, tools in structure %}
|
||||
|
||||
### {{ platform }}
|
||||
|
||||
{% for tool in tools %}
|
||||
#### {{ tool.title }}
|
||||
|
||||
{% if tool.note %}
|
||||
{{ tool.note }}
|
||||
{% endif %}
|
||||
|
||||
{% if tool.links.setup %}
|
||||
To install [{{ tool.name }}]({{ tool.links.homepage }}) follow [installation instructions]({{ tool.links.setup }}).
|
||||
{% endif %}
|
||||
|
||||
```bash
|
||||
# Install
|
||||
$ {{ tool.commands.install|join('\n$ ') }}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Upgrade
|
||||
$ {{ tool.commands.upgrade|join('\n$ ') }}
|
||||
```
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
<!-- /GENERATED SECTION -->
|
269
docs/installation/methods.yml
Normal file
269
docs/installation/methods.yml
Normal file
@ -0,0 +1,269 @@
|
||||
# Database of HTTPie installation methods. Used to build the docs.
|
||||
#
|
||||
# We currently only include here methods for popular systems where we take care of the package,
|
||||
# or have a good relationship with the maintainers.
|
||||
#
|
||||
# Each tool name should be unique (it becomes a linkable header).
|
||||
# If a tools have `links.setup`, it also needs `links.homepage`.
|
||||
# Some tools are available on multiple platforms, take into account when editing.
|
||||
#
|
||||
|
||||
docs-structure:
|
||||
Universal:
|
||||
- pypi
|
||||
macOS:
|
||||
- brew-mac
|
||||
- port
|
||||
- snap-mac
|
||||
- spack-mac
|
||||
Windows:
|
||||
- chocolatey
|
||||
Linux:
|
||||
- snap-linux
|
||||
- brew-linux
|
||||
- apt
|
||||
- dnf
|
||||
- yum
|
||||
- apk
|
||||
- emerge
|
||||
- pacman
|
||||
- xbps-install
|
||||
- spack-linux
|
||||
FreeBSD:
|
||||
- pkg
|
||||
|
||||
tools:
|
||||
apk:
|
||||
title: Alpine Linux
|
||||
name: apk
|
||||
links:
|
||||
homepage: https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management
|
||||
package: https://pkgs.alpinelinux.org/package/edge/community/x86/httpie
|
||||
commands:
|
||||
install:
|
||||
- apk update
|
||||
- apk add httpie
|
||||
upgrade:
|
||||
- apk update
|
||||
- apk add --upgrade httpie
|
||||
|
||||
apt:
|
||||
title: Debian and Ubuntu
|
||||
note: Also works for other Debian-derived distributions like MX Linux, Linux Mint, deepin, Pop!_OS, KDE neon, Zorin OS, elementary OS, Kubuntu, Devuan, Linux Lite, Peppermint OS, Lubuntu, antiX, Xubuntu, etc.
|
||||
name: APT
|
||||
links:
|
||||
homepage: https://en.wikipedia.org/wiki/APT_(software)
|
||||
package: https://packages.debian.org/sid/web/httpie
|
||||
commands:
|
||||
install:
|
||||
- apt update
|
||||
- apt install httpie
|
||||
upgrade:
|
||||
- apt update
|
||||
- apt upgrade httpie
|
||||
|
||||
brew-mac:
|
||||
title: Homebrew
|
||||
name: Homebrew
|
||||
links:
|
||||
homepage: https://brew.sh/
|
||||
setup: https://docs.brew.sh/Installation
|
||||
package: https://formulae.brew.sh/formula/httpie
|
||||
commands:
|
||||
install:
|
||||
- brew update
|
||||
- brew install httpie
|
||||
upgrade:
|
||||
- brew update
|
||||
- brew upgrade httpie
|
||||
|
||||
brew-linux:
|
||||
title: Linuxbrew
|
||||
name: Linuxbrew
|
||||
links:
|
||||
homepage: https://docs.brew.sh/Homebrew-on-Linux
|
||||
setup: https://docs.brew.sh/Homebrew-on-Linux#install
|
||||
package: https://formulae.brew.sh/formula/httpie
|
||||
commands:
|
||||
install:
|
||||
- brew update
|
||||
- brew install httpie
|
||||
upgrade:
|
||||
- brew update
|
||||
- brew upgrade httpie
|
||||
|
||||
chocolatey:
|
||||
title: Chocolatey
|
||||
name: Chocolatey
|
||||
links:
|
||||
homepage: https://chocolatey.org/
|
||||
setup: https://chocolatey.org/install
|
||||
package: https://community.chocolatey.org/packages/httpie/
|
||||
commands:
|
||||
install:
|
||||
- choco install httpie
|
||||
upgrade:
|
||||
- choco upgrade httpie
|
||||
|
||||
dnf:
|
||||
title: Fedora
|
||||
name: DNF
|
||||
links:
|
||||
homepage: https://fedoraproject.org/wiki/DNF
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- dnf update
|
||||
- dnf install httpie
|
||||
upgrade:
|
||||
- dnf update
|
||||
- dnf upgrade httpie
|
||||
|
||||
emerge:
|
||||
title: Gentoo
|
||||
name: Portage
|
||||
links:
|
||||
homepage: https://wiki.gentoo.org/wiki/Portage
|
||||
package: https://packages.gentoo.org/packages/net-misc/httpie
|
||||
commands:
|
||||
install:
|
||||
- emerge --sync
|
||||
- emerge httpie
|
||||
upgrade:
|
||||
- emerge --sync
|
||||
- emerge --update httpie
|
||||
|
||||
pacman:
|
||||
title: Arch Linux
|
||||
name: pacman
|
||||
note: Also works for other Arch-derived distributions like ArcoLinux, EndeavourOS, Artix Linux, etc.
|
||||
links:
|
||||
homepage: https://archlinux.org/pacman/
|
||||
package: https://archlinux.org/packages/community/any/httpie/
|
||||
commands:
|
||||
install:
|
||||
- pacman -Sy httpie
|
||||
upgrade:
|
||||
- pacman -Syu httpie
|
||||
|
||||
pkg:
|
||||
title: FreshPorts
|
||||
name: FreshPorts
|
||||
links:
|
||||
homepage: https://www.freebsd.org/cgi/man.cgi?query=pkg&sektion=8&n=1
|
||||
package: https://www.freshports.org/www/py-httpie/
|
||||
commands:
|
||||
install:
|
||||
- pkg install www/py-httpie
|
||||
upgrade:
|
||||
- pkg upgrade www/py-httpie
|
||||
|
||||
port:
|
||||
title: MacPorts
|
||||
name: MacPorts
|
||||
links:
|
||||
homepage: https://www.macports.org/
|
||||
setup: https://www.macports.org/install.php
|
||||
package: https://ports.macports.org/port/httpie/
|
||||
commands:
|
||||
install:
|
||||
- port selfupdate
|
||||
- port install httpie
|
||||
upgrade:
|
||||
- port selfupdate
|
||||
- port upgrade httpie
|
||||
|
||||
pypi:
|
||||
title: PyPi
|
||||
name: pip
|
||||
note: Please make sure you have Python 3.6 or newer (`python --version`).
|
||||
links:
|
||||
homepage: https://pypi.org/
|
||||
# setup: https://pip.pypa.io/en/stable/installation/
|
||||
package: https://pypi.org/project/httpie/
|
||||
commands:
|
||||
install:
|
||||
- python -m pip install --upgrade pip wheel
|
||||
- python -m pip install httpie
|
||||
upgrade:
|
||||
- python -m pip install --upgrade pip wheel
|
||||
- python -m pip install --upgrade httpie
|
||||
|
||||
snap-linux:
|
||||
title: Snapcraft (Linux)
|
||||
name: Snapcraft
|
||||
links:
|
||||
homepage: https://snapcraft.io/
|
||||
setup: https://snapcraft.io/docs/installing-snapd
|
||||
package: https://snapcraft.io/httpie
|
||||
commands:
|
||||
install:
|
||||
- snap install httpie
|
||||
upgrade:
|
||||
- snap refresh httpie
|
||||
|
||||
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:
|
||||
title: CentOS and RHEL
|
||||
name: Yum
|
||||
note: Also works for other RHEL-derived distributions like ClearOS, Oracle Linux, etc.
|
||||
links:
|
||||
homepage: http://yum.baseurl.org/
|
||||
package: https://src.fedoraproject.org/rpms/httpie
|
||||
commands:
|
||||
install:
|
||||
- yum update
|
||||
- yum install epel-release
|
||||
- yum install httpie
|
||||
upgrade:
|
||||
- yum update
|
||||
- yum upgrade httpie
|
41
docs/markdownlint.rb
Normal file
41
docs/markdownlint.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# Rules for <https://github.com/markdownlint/markdownlint>
|
||||
|
||||
# Load all rules by default
|
||||
all
|
||||
|
||||
#
|
||||
# Tweak rules
|
||||
#
|
||||
|
||||
# MD002 First header should be a top level header
|
||||
# Because we use HTML to hide them on the website.
|
||||
exclude_rule 'MD002'
|
||||
|
||||
# MD013 Line length
|
||||
exclude_rule 'MD013'
|
||||
|
||||
# MD014 Dollar signs used before commands without showing output
|
||||
exclude_rule 'MD014'
|
||||
|
||||
# Tell the linter to use ordered lists:
|
||||
# 1. Foo
|
||||
# 2. Bar
|
||||
# 3. Baz
|
||||
#
|
||||
# Instead of:
|
||||
# 1. Foo
|
||||
# 1. Bar
|
||||
# 1. Baz
|
||||
rule 'MD029', :style => :ordered
|
||||
|
||||
# MD033 Inline HTML
|
||||
# TODO: Tweak elements when https://github.com/markdownlint/markdownlint/issues/118 will be done?
|
||||
exclude_rule 'MD033'
|
||||
|
||||
# MD034 Bare URL used
|
||||
# TODO: Remove when https://github.com/markdownlint/markdownlint/issues/328 will be fixed.
|
||||
exclude_rule 'MD034'
|
||||
|
||||
# MD041 First line in file should be a top level header
|
||||
# Because we use HTML to hide them on the website.
|
||||
exclude_rule 'MD041'
|
49
docs/packaging/README.md
Normal file
49
docs/packaging/README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# HTTPie release process
|
||||
|
||||
Welcome on the documentation part of the **HTTPie release process**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions, then you can find all you need for your OS on [that page](https://httpie.io/docs#installation). In the case you do not find your OS, [let us know](https://github.com/httpie/httpie/issues/).
|
||||
- If you are looking for technical information about the HTTPie packaging, then you are at the good place.
|
||||
|
||||
## About
|
||||
|
||||
You are looking at the HTTPie packaging documentation, where you will find valuable information about how we manage to release HTTPie to lots of OSes, including technical data that may be worth reading if you are a package maintainer.
|
||||
|
||||
The overall release process starts simple:
|
||||
|
||||
1. Do the [PyPi](https://pypi.org/project/httpie/) publication.
|
||||
2. Then, handle company-related tasks.
|
||||
3. Finally, follow OS-specific steps, described in documents below, to send patches downstream.
|
||||
|
||||
## First, PyPi
|
||||
|
||||
Let's do the release on [PyPi](https://pypi.org/project/httpie/).
|
||||
That is done quite easily by manually triggering the [release workflow](https://github.com/httpie/httpie/actions/workflows/release.yml).
|
||||
|
||||
## Then, company-specific tasks
|
||||
|
||||
- Update the HTTPie version bundled into termible ([example](https://github.com/httpie/termible/pull/1)).
|
||||
|
||||
## Finally, spread dowstream
|
||||
|
||||
Find out how we do release new versions for each and every supported OS in the following table.
|
||||
A more complete state of deployment can be found on [repology](https://repology.org/project/httpie/versions), including unofficial packages.
|
||||
|
||||
| OS | Maintainer |
|
||||
| -------------------------------------------: | -------------- |
|
||||
| [Alpine](linux-alpine/) | **HTTPie** |
|
||||
| [Arch Linux, and derived](linux-arch/) | trusted person |
|
||||
| :construction: [AOSC OS](linux-aosc/) | **HTTPie** |
|
||||
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
|
||||
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
|
||||
| [Fedora](linux-fedora/) | trusted person |
|
||||
| [Gentoo](linux-gentoo/) | **HTTPie** |
|
||||
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
|
||||
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
|
||||
| [Snapcraft](snapcraft/) | **HTTPie** |
|
||||
| [Spack](spack/) | **HTTPie** |
|
||||
| [Void Linux](linux-void/) | **HTTPie** |
|
||||
| [Windows — Chocolatey](windows-chocolatey/) | **HTTPie** |
|
||||
|
||||
:new: You do not find your system or you would like to see HTTPie supported on another OS? Then [let us know](https://github.com/httpie/httpie/issues/).
|
33
docs/packaging/brew/README.md
Normal file
33
docs/packaging/brew/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# HTTPie on Homebrew, and Linuxbrew
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Homebrew**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Homebrew, then you can find them on [that page](https://httpie.io/docs#homebrew) ([that one](https://httpie.io/docs#linuxbrew) for Linuxbrew).
|
||||
- If you are looking for technical information about the HTTPie packaging on Homebrew, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Homebrew. They apply to Linuxbrew as well.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
:construction: Work in progress.
|
||||
|
||||
First, update the current Formula:
|
||||
|
||||
```bash
|
||||
make brew-deps
|
||||
# Copy-paste content into downstream/mac/brew/httpie.rb
|
||||
git add downstream/mac/brew/httpie.rb
|
||||
git commit -s -m 'Update brew formula to XXX'
|
||||
```
|
||||
|
||||
That [GitHub workflow](https://github.com/httpie/httpie/actions/workflows/test-package-mac-brew.yml) will test the formula when `downstream/mac/brew/httpie.rb` is changed in a pull request.
|
||||
|
||||
Then, open a pull request with those changes to the [downstream file]([ file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb)).
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
@ -1,32 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate URLs and file hashes to be included in the Homebrew formula
|
||||
after a new release of HTTPie has been published on PyPi.
|
||||
Generate Ruby code with URLs and file hashes for packages from PyPi
|
||||
(i.e., httpie itself as well as its dependencies) to be included
|
||||
in the Homebrew formula after a new release of HTTPie has been published
|
||||
on PyPi.
|
||||
|
||||
https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
<https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb>
|
||||
|
||||
"""
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
|
||||
VERSIONS = {
|
||||
# By default, we use the latest packages. But sometimes Requests has a maximum supported versions.
|
||||
# Take a look here before making a release: <https://github.com/psf/requests/blob/master/setup.py>
|
||||
'idna': '3.2',
|
||||
}
|
||||
|
||||
|
||||
# Note: Keep that list sorted.
|
||||
PACKAGES = [
|
||||
'httpie',
|
||||
'pygments',
|
||||
'requests',
|
||||
'certifi',
|
||||
'urllib3',
|
||||
'charset-normalizer',
|
||||
'defusedxml',
|
||||
'httpie',
|
||||
'idna',
|
||||
'chardet',
|
||||
'Pygments',
|
||||
'PySocks',
|
||||
'requests',
|
||||
'requests-toolbelt',
|
||||
'urllib3',
|
||||
]
|
||||
|
||||
|
||||
def get_package_meta(package_name):
|
||||
api_url = 'https://pypi.python.org/pypi/{}/json'.format(package_name)
|
||||
api_url = f'https://pypi.org/pypi/{package_name}/json'
|
||||
resp = requests.get(api_url).json()
|
||||
hasher = hashlib.sha256()
|
||||
for release in resp['urls']:
|
||||
version = VERSIONS.get(package_name)
|
||||
if package_name not in VERSIONS:
|
||||
# Latest version
|
||||
release_bundle = resp['urls']
|
||||
else:
|
||||
release_bundle = resp['releases'][version]
|
||||
|
||||
for release in release_bundle:
|
||||
download_url = release['url']
|
||||
if download_url.endswith('.tar.gz'):
|
||||
hasher.update(requests.get(download_url).content)
|
||||
@ -36,8 +55,7 @@ def get_package_meta(package_name):
|
||||
'sha256': hasher.hexdigest(),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'{}: download not found: {}'.format(package_name, resp))
|
||||
raise RuntimeError(f'{package_name}: download not found: {resp}')
|
||||
|
||||
|
||||
def main():
|
74
docs/packaging/brew/httpie.rb
Normal file
74
docs/packaging/brew/httpie.rb
Normal file
@ -0,0 +1,74 @@
|
||||
class Httpie < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.io/"
|
||||
url "https://files.pythonhosted.org/packages/90/64/7ea8066309970f787653bdc8c5328272a5c4d06cbce3a07a6a5c3199c3d7/httpie-2.5.0.tar.gz"
|
||||
sha256 "fe6a8bc50fb0635a84ebe1296a732e39357c3e1354541bf51a7057b4877e47f9"
|
||||
license "BSD-3-Clause"
|
||||
head "https://github.com/httpie/httpie.git"
|
||||
|
||||
bottle do
|
||||
sha256 cellar: :any_skip_relocation, arm64_big_sur: "01115f69aff0399b3f73af09899a42a14343638a4624a35749059cc732c49cdc"
|
||||
sha256 cellar: :any_skip_relocation, big_sur: "53f07157f00edf8193b7d4f74f247f53e1796fbc3e675cd2fbaa4b9dc2bad62c"
|
||||
sha256 cellar: :any_skip_relocation, catalina: "7cf216fdee98208856d654060fdcad3968623d7ed27fcdeba27d3120354c9a9f"
|
||||
sha256 cellar: :any_skip_relocation, mojave: "28adb5aed8c1c2b39c51789f242ff0dffde39073e161deb379c79184d787d063"
|
||||
sha256 cellar: :any_skip_relocation, x86_64_linux: "91cb8c332c643bd8b1d0a8f3ec0acd4770b407991f6de1fd320d675f2b2e95ec"
|
||||
end
|
||||
|
||||
depends_on "python@3.9"
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/6d/78/f8db8d57f520a54f0b8a438319c342c61c22759d8f9a1cd2e2180b5e5ea9/certifi-2021.5.30.tar.gz"
|
||||
sha256 "2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"
|
||||
end
|
||||
|
||||
resource "charset-normalizer" do
|
||||
url "https://files.pythonhosted.org/packages/e7/4e/2af0238001648ded297fb54ceb425ca26faa15b341b4fac5371d3938666e/charset-normalizer-2.0.4.tar.gz"
|
||||
sha256 "f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||
end
|
||||
|
||||
resource "defusedxml" do
|
||||
url "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz"
|
||||
sha256 "1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz"
|
||||
sha256 "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
end
|
||||
|
||||
resource "Pygments" do
|
||||
url "https://files.pythonhosted.org/packages/b7/b3/5cba26637fe43500d4568d0ee7b7362de1fb29c0e158d50b4b69e9a40422/Pygments-2.10.0.tar.gz"
|
||||
sha256 "f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
url "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz"
|
||||
sha256 "3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/e7/01/3569e0b535fb2e4a6c384bdbed00c55b9d78b5084e0fb7f4d0bf523d7670/requests-2.26.0.tar.gz"
|
||||
sha256 "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
end
|
||||
|
||||
resource "requests-toolbelt" do
|
||||
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
|
||||
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/4f/5a/597ef5911cb8919efe4d86206aa8b2658616d676a7088f0825ca08bd7cb8/urllib3-1.26.6.tar.gz"
|
||||
sha256 "f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
||||
end
|
||||
|
||||
def install
|
||||
virtualenv_install_with_resources
|
||||
end
|
||||
|
||||
test do
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
end
|
||||
end
|
33
docs/packaging/linux-alpine/APKBUILD
Normal file
33
docs/packaging/linux-alpine/APKBUILD
Normal file
@ -0,0 +1,33 @@
|
||||
# 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"
|
67
docs/packaging/linux-alpine/README.md
Normal file
67
docs/packaging/linux-alpine/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
# 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
|
||||
```
|
24
docs/packaging/linux-aosc/README.md
Normal file
24
docs/packaging/linux-aosc/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 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.
|
5
docs/packaging/linux-aosc/spec
Normal file
5
docs/packaging/linux-aosc/spec
Normal file
@ -0,0 +1,5 @@
|
||||
VER=2.5.0
|
||||
SRCS="tbl::https://github.com/httpie/httpie/archive/$VER.tar.gz"
|
||||
CHKSUMS="sha256::66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea"
|
||||
REL=1
|
||||
CHKUPDATE="anitya::id=1337"
|
46
docs/packaging/linux-arch/PKGBUILD
Normal file
46
docs/packaging/linux-arch/PKGBUILD
Normal file
@ -0,0 +1,46 @@
|
||||
# Maintainer: Jelle van der Waa <jelle@archlinux.org>
|
||||
# Maintainer: daurnimator <daurnimator@archlinux.org>
|
||||
# Contributor: Daniel Micay <danielmicay@gmail.com>
|
||||
# Contributor: Thomas Weißschuh <thomas_weissschuh lavabit com>
|
||||
|
||||
pkgname=httpie
|
||||
pkgver=2.5.0
|
||||
pkgrel=1
|
||||
pkgdesc="human-friendly CLI HTTP client for the API era"
|
||||
url="https://github.com/httpie/httpie"
|
||||
depends=('python-defusedxml'
|
||||
'python-pygments'
|
||||
'python-pysocks'
|
||||
'python-requests'
|
||||
'python-requests-toolbelt')
|
||||
makedepends=('python-setuptools')
|
||||
checkdepends=('python-pytest'
|
||||
'python-pytest-httpbin'
|
||||
'python-responses')
|
||||
conflicts=(python-httpie)
|
||||
replaces=(python-httpie python2-httpie)
|
||||
license=('BSD')
|
||||
arch=('any')
|
||||
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
|
||||
sha256sums=('66af56e0efc1ca6237323f1186ba34bca1be24e67a4319fd5df7228ab986faea')
|
||||
|
||||
build() {
|
||||
cd $pkgname-$pkgver
|
||||
python3 setup.py build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $pkgname-$pkgver
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/httpie/LICENSE"
|
||||
python3 setup.py install --root="$pkgdir" --optimize=1
|
||||
|
||||
# Fix upstream, include them in MANIFEST.in and use data_files in setup.py to install them automatically
|
||||
# TODO: add zsh support
|
||||
install -Dm644 extras/httpie-completion.bash "$pkgdir"/usr/share/bash-completion/completions/http
|
||||
install -Dm644 extras/httpie-completion.fish "$pkgdir"/usr/share/fish/vendor_completions.d/http.fish
|
||||
}
|
||||
|
||||
check() {
|
||||
cd $pkgname-$pkgver
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 setup.py test
|
||||
}
|
22
docs/packaging/linux-arch/README.md
Normal file
22
docs/packaging/linux-arch/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# HTTPie on Arch Linux, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Arch Linux**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Arch Linux, then you can find them on [that page](https://httpie.io/docs#arch-linux).
|
||||
- If you are looking for technical information about the HTTPie packaging on Arch Linux, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Arch Linux. They apply to Arch-derived distributions as well, like ArcoLinux, EndeavourOS, Artix Linux, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
Note: Sending patches downstream does not seem easy. We failed to find where is located the package file on <https://gitlab.archlinux.org>. So we are relying on the last maintainer, daurnimator, and it works pretty well so far.
|
||||
|
||||
Check <https://archlinux.org/packages/community/any/httpie/> and if the version is outdated, simply [report it](https://archlinux.org/packages/community/any/httpie/flag/).
|
||||
|
||||
## Hacking
|
||||
|
||||
Left blank on purpose, we will fill that section when we will have access to the downstream repository.
|
26
docs/packaging/linux-centos/README.md
Normal file
26
docs/packaging/linux-centos/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# HTTPie on CentOS, RHEL, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for CentOS and RHEL**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on CentOS, then you can find them on [that page](https://httpie.io/docs#centos-and-rhel).
|
||||
- If you are looking for technical information about the HTTPie packaging on CentOS, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for CentOS. They apply to RHEL as well, and any RHEL-derived distributions like ClearOS, Oracle Linux, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is [Mikel Olasagasti](https://github.com/kaxero).
|
||||
|
||||
## Overall process
|
||||
|
||||
Same as [Fedora](../linux-fedora/README.md#overall-process).
|
||||
|
||||
## Q/A with Mikel
|
||||
|
||||
Q: What should we do to help seeing a new version on CentOS?
|
||||
|
||||
A: When a new release is published Miro and I get notified by [release-monitoring](https://release-monitoring.org/project/1337/), that fills a BugZilla ticket reporting a new version being available.
|
||||
|
||||
The system also tries to create a simple patch to update the spec file, but in the case of CentOS it needs some manual revision. For example for 2.5.0 `defuxedxml` dep is required. Maybe with CentOS-9 and some new macros that are available now in Fedora it can be automated same way. But even the bump can be automated, maintainers should check for license changes, new binaries/docs/ and so on.
|
29
docs/packaging/linux-debian/README.md
Normal file
29
docs/packaging/linux-debian/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# HTTPie on Debian, Ubuntu, and derived
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Debian GNU/Linux**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Debian GNU/Linux, then you can find them on [that page](https://httpie.io/docs#debian-and-ubuntu).
|
||||
- If you are looking for technical information about the HTTPie packaging on Debian GNU/Linux, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Debian GNU/Linux. They apply to Ubuntu as well, and any Debian-derived distributions like MX Linux, Linux Mint, deepin, Pop!_OS, KDE neon, Zorin OS, elementary OS, Kubuntu, Devuan, Linux Lite, Peppermint OS, Lubuntu, antiX, Xubuntu, etc.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is Bartosz Fenski.
|
||||
|
||||
## Overall process
|
||||
|
||||
Open a new bug on the Debian Bug Tracking System by sending an email:
|
||||
|
||||
- To: `Debian Bug Tracking System <submit@bugs.debian.org>`
|
||||
- Subject: `httpie: Version XXX available`
|
||||
- Message template ([example](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=993937)):
|
||||
|
||||
```email
|
||||
Package: httpie
|
||||
Severity: wishlist
|
||||
|
||||
<MESSAGE>
|
||||
```
|
48
docs/packaging/linux-fedora/README.md
Normal file
48
docs/packaging/linux-fedora/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# HTTPie on Fedora
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Fedora**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Fedora, then you can find them on [that page](https://httpie.io/docs#fedora).
|
||||
- If you are looking for technical information about the HTTPie packaging on Fedora, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Fedora.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
The current maintainer is [Miro Hrončok](https://github.com/hroncok).
|
||||
|
||||
## Overall process
|
||||
|
||||
We added the [.packit.yaml](https://github.com/httpie/httpie/blob/master/.packit.yaml) local file.
|
||||
It unlocks real-time Fedora checks on pull requests and new releases.
|
||||
|
||||
So there is nothing to do on our side: `Packit` will see the new release and open a pull request [there](https://src.fedoraproject.org/rpms/httpie). Then, the Fedora maintainer will review and merge.
|
||||
|
||||
It is also possible to follow [user feedbacks](https://bodhi.fedoraproject.org/updates/?packages=httpie) for all builds.
|
||||
|
||||
## Q/A with Miro
|
||||
|
||||
Q: What would the command to install the latest stable version look like?
|
||||
|
||||
A: Assuming the latest stable version is already propagated to Fedora:
|
||||
|
||||
```bash
|
||||
# Note that yum is an alias to dnf.
|
||||
$ sudo dnf install httpie
|
||||
```
|
||||
|
||||
Q: Will dnf/yum upgrade then update to the latest?
|
||||
|
||||
A: Yes, assuming the same as above.
|
||||
|
||||
Q: Are new versions backported automatically?
|
||||
|
||||
A: No. The process is:
|
||||
|
||||
1. A new HTTPie release is created on Github.
|
||||
2. A pull request for Fedora `rawhide` (the development version of Fedora, currently Fedora 35) is created.
|
||||
3. A Fedora packager (usually Miro) sanity checks the pull request and merges, builds. HTTPie is updated in `rawhide` within 24 hours (sometimes more, for unrelated issues).
|
||||
4. A Fedora packager decides whether the upgrade is suitable for stable Fedora releases (currently 34, 33), if so, merges the changes there.
|
||||
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.
|
2
docs/packaging/linux-gentoo/Manifest
Normal file
2
docs/packaging/linux-gentoo/Manifest
Normal file
@ -0,0 +1,2 @@
|
||||
DIST httpie-2.4.0.tar.gz 1772537 BLAKE2B 111451cc7dc353d5b586554f98ac715a3198f03e74d261944a5f021d2dcc948455500800222b323d182a2a067d0549bda7c318ab3a6c934b9a9beec64aff2db2 SHA512 44cc7ff4fe0f3d8c53a7dd750465f6b56c36f5bbac06d22b760579bd60949039e82313845699669a659ec91adc69dbeac22c06ddd63af64e6f2e0edecf3e732a
|
||||
DIST httpie-2.5.0.tar.gz 1105177 BLAKE2B 6e16868c81522d4e6d2fc0a4e093c190f18ced720b35217930865ae3f8e168193cc33dfecc13c5d310f52647d6e79d17b247f56e56e8586d633a2d9502be66a7 SHA512 f14aa23fea7578181b9bd6ededea04de9ddf0b2f697b23f76d2d96e2c17b95617318c711750bad6af550400dbc03732ab17fdf84e59d577f33f073e600a55330
|
78
docs/packaging/linux-gentoo/README.md
Normal file
78
docs/packaging/linux-gentoo/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# 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
|
||||
```
|
42
docs/packaging/linux-gentoo/httpie-2.5.0.ebuild
Normal file
42
docs/packaging/linux-gentoo/httpie-2.5.0.ebuild
Normal file
@ -0,0 +1,42 @@
|
||||
# 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
|
||||
}
|
28
docs/packaging/linux-gentoo/metadata.xml
Normal file
28
docs/packaging/linux-gentoo/metadata.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?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>
|
68
docs/packaging/linux-void/README.md
Normal file
68
docs/packaging/linux-void/README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# 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
|
||||
```
|
28
docs/packaging/linux-void/template
Normal file
28
docs/packaging/linux-void/template
Normal file
@ -0,0 +1,28 @@
|
||||
# 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}"
|
||||
}
|
49
docs/packaging/mac-ports/Portfile
Normal file
49
docs/packaging/mac-ports/Portfile
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8::et:sw=4:ts=4:sts=4
|
||||
|
||||
PortSystem 1.0
|
||||
PortGroup github 1.0
|
||||
PortGroup python 1.0
|
||||
|
||||
github.setup httpie httpie 2.5.0
|
||||
|
||||
maintainers {g5pw @g5pw} openmaintainer
|
||||
categories net
|
||||
description HTTPie is a command line HTTP client, a user-friendly cURL replacement.
|
||||
long_description HTTPie (pronounced aych-tee-tee-pie) is a command line HTTP \
|
||||
client. Its goal is to make CLI interaction with web \
|
||||
services as human-friendly as possible. It provides a simple \
|
||||
http command that allows for sending arbitrary HTTP requests \
|
||||
using a simple and natural syntax, and displays colorized \
|
||||
responses. HTTPie can be used for testing, debugging, and \
|
||||
generally interacting with HTTP servers.
|
||||
platforms darwin
|
||||
license BSD
|
||||
homepage https://httpie.io/
|
||||
|
||||
variant python36 conflicts python37 python38 python39 description "Use Python 3.6" {}
|
||||
variant python37 conflicts python36 python38 python39 description "Use Python 3.7" {}
|
||||
variant python38 conflicts python36 python37 python39 description "Use Python 3.8" {}
|
||||
variant python39 conflicts python36 python37 python38 description "Use Python 3.9" {}
|
||||
|
||||
if {[variant_isset python36]} {
|
||||
python.default_version 36
|
||||
} elseif {[variant_isset python37]} {
|
||||
python.default_version 37
|
||||
} elseif {[variant_isset python39]} {
|
||||
python.default_version 39
|
||||
} else {
|
||||
default_variants +python38
|
||||
python.default_version 38
|
||||
}
|
||||
|
||||
depends_lib-append port:py${python.version}-requests \
|
||||
port:py${python.version}-requests-toolbelt \
|
||||
port:py${python.version}-pygments \
|
||||
port:py${python.version}-socks \
|
||||
port:py${python.version}-defusedxml
|
||||
|
||||
checksums rmd160 88d227d52199c232c0ddf704a219d1781b1e77ee \
|
||||
sha256 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc \
|
||||
size 1105185
|
||||
|
||||
python.link_binaries_suffix
|
40
docs/packaging/mac-ports/README.md
Normal file
40
docs/packaging/mac-ports/README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# HTTPie on MacPorts
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for MacPorts**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on MacPorts, then you can find them on [that page](https://httpie.io/docs#macports).
|
||||
- If you are looking for technical information about the HTTPie packaging on MacPorts, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for MacPorts.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
Open a pull request to update the [downstream file](https://github.com/macports/macports-ports/blob/master/net/httpie/Portfile) ([example](https://github.com/macports/macports-ports/pull/12167)).
|
||||
|
||||
- Here is how to calculate the size and checksums (replace `2.5.0` with the correct version):
|
||||
|
||||
```bash
|
||||
# Download the archive
|
||||
$ wget https://api.github.com/repos/httpie/httpie/tarball/2.5.0
|
||||
|
||||
# Size
|
||||
$ stat --printf="%s\n" 2.5.0
|
||||
1105185
|
||||
|
||||
# Checksums
|
||||
$ openssl dgst -rmd160 2.5.0
|
||||
RIPEMD160(2.5.0)= 88d227d52199c232c0ddf704a219d1781b1e77ee
|
||||
$ openssl dgst -sha256 2.5.0
|
||||
SHA256(2.5.0)= 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc
|
||||
```
|
||||
|
||||
- The commit message must be `httpie: update to XXX`.
|
||||
- The commit must be signed-off (`git commit -s`).
|
||||
|
||||
## Hacking
|
||||
|
||||
:construction: Work in progress.
|
51
docs/packaging/snapcraft/README.md
Normal file
51
docs/packaging/snapcraft/README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# HTTPie on Snapcraft
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Snapcraft**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Snapcraft, then you can find them on [that page](https://httpie.io/docs#snapcraft-linux) ([that one](https://httpie.io/docs#snapcraft-macos) for macOS).
|
||||
- If you are looking for technical information about the HTTPie packaging on Snapcraft, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Snapcraft. They apply to Snapcraft on Linux, macOS, and Windows.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
Trigger a new [build](https://snapcraft.io/httpie/builds), then [promote it](https://snapcraft.io/httpie/releases). If more management is needed: [revisions supervision](https://dashboard.snapcraft.io/snaps/httpie/revisions/).
|
||||
|
||||
## Hacking
|
||||
|
||||
Launch the docker image:
|
||||
|
||||
```bash
|
||||
docker pull ubuntu/latest
|
||||
docker run -it --rm ubuntu/latest
|
||||
```
|
||||
|
||||
From inside the container:
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone --depth=1 https://github.com/httpie/httpie.git
|
||||
cd httpie
|
||||
|
||||
# Build
|
||||
export SNAPCRAFT_BUILD_ENVIRONMENT_CPU=8
|
||||
export SNAPCRAFT_BUILD_ENVIRONMENT_MEMORY=16G
|
||||
snapcraft --debug
|
||||
|
||||
# Install
|
||||
sudo snap install --dangerous httpie_XXX_amd64.snap
|
||||
|
||||
# Test
|
||||
httpie.http --version
|
||||
httpie.https --version
|
||||
# Auto-aliases cannot be tested when installing a snap outside the store.
|
||||
# http --version
|
||||
# https --version
|
||||
|
||||
# Remove
|
||||
sudo snap remove httpie
|
||||
```
|
54
docs/packaging/spack/README.md
Normal file
54
docs/packaging/spack/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# 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
|
||||
```
|
32
docs/packaging/spack/package.py
Normal file
32
docs/packaging/spack/package.py
Normal file
@ -0,0 +1,32 @@
|
||||
# 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')
|
45
docs/packaging/windows-chocolatey/README.md
Normal file
45
docs/packaging/windows-chocolatey/README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# HTTPie on Chocolatey
|
||||
|
||||
Welcome to the documentation about **packaging HTTPie for Chocolatey**.
|
||||
|
||||
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
|
||||
- If you are looking for HTTPie installation or upgrade instructions on Chocolatey, then you can find them on [that page](https://httpie.io/docs#chocolatey).
|
||||
- If you are looking for technical information about the HTTPie packaging on Chocolatey, then you are in a good place.
|
||||
|
||||
## About
|
||||
|
||||
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Chocolatey.
|
||||
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
|
||||
|
||||
## Overall process
|
||||
|
||||
After having successfully [built and tested](#hacking) the package, push it:
|
||||
|
||||
```bash
|
||||
# Replace 2.5.0 with the correct version
|
||||
choco push httpie.2.5.0.nupkg -s https://push.chocolatey.org/ --api-key=API_KEY
|
||||
```
|
||||
|
||||
## Hacking
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone --depth=1 https://github.com/httpie/httpie.git
|
||||
cd httpie/docs/packaging/windows-chocolatey
|
||||
|
||||
# Build
|
||||
choco pack
|
||||
|
||||
# Check metadata
|
||||
choco info httpie -s .
|
||||
|
||||
# Install
|
||||
choco install httpie -y -dv -s "'.;https://community.chocolatey.org/api/v2/'"
|
||||
|
||||
# Test
|
||||
http --version
|
||||
https --version
|
||||
|
||||
# Remove
|
||||
choco uninstall -y httpie
|
||||
```
|
50
docs/packaging/windows-chocolatey/httpie.nuspec
Normal file
50
docs/packaging/windows-chocolatey/httpie.nuspec
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>httpie</id>
|
||||
<version>2.5.0</version>
|
||||
<summary>Modern, user-friendly command-line HTTP client for the API era.</summary>
|
||||
<description>
|
||||
HTTPie *aitch-tee-tee-pie* is a user-friendly command-line HTTP client for the API era.
|
||||
It comes with JSON support, syntax highlighting, persistent sessions, wget-like downloads, plugins, and more.
|
||||
|
||||
The project's goal is to make CLI interaction with web services as human-friendly as possible. HTTPie is designed for testing, debugging, and generally interacting with APIs and HTTP servers.
|
||||
The `http` and `https` commands allow for creating and sending arbitrary HTTP requests. They use simple and natural syntax and provide formatted and colorized output.
|
||||
|
||||
Main features:
|
||||
|
||||
- Built-in JSON support
|
||||
- Colorized and formatted terminal output
|
||||
- Sensible defaults for the API era
|
||||
- Persistent sessions
|
||||
- Forms and file uploads
|
||||
- HTTPS, proxies, and authentication support
|
||||
- Support for arbitrary request data and headers
|
||||
- Wget-like downloads
|
||||
- Extensions API
|
||||
- Expressive and intuitive syntax
|
||||
- Linux, macOS, Windows, and FreeBSD support
|
||||
- All that and more in 2 simple commands: `http` + `https`
|
||||
</description>
|
||||
<title>HTTPie</title>
|
||||
<authors>HTTPie</authors>
|
||||
<owners>Tiger-222</owners>
|
||||
<copyright>2012-2021 Jakub Roztocil</copyright>
|
||||
<licenseUrl>https://raw.githubusercontent.com/httpie/httpie/master/LICENSE</licenseUrl>
|
||||
<iconUrl>https://pie-assets.s3.eu-central-1.amazonaws.com/LogoIcons/GB.png</iconUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<releaseNotes>See the [changelog](https://github.com/httpie/httpie/blob/2.5.0/CHANGELOG.md).</releaseNotes>
|
||||
<tags>httpie http https rest api client curl python ssl cli foss oss url</tags>
|
||||
<projectUrl>https://httpie.io</projectUrl>
|
||||
<packageSourceUrl>https://github.com/httpie/httpie</packageSourceUrl>
|
||||
<projectSourceUrl>https://github.com/httpie/httpie</projectSourceUrl>
|
||||
<docsUrl>https://httpie.io/docs</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/httpie/httpie/issues</bugTrackerUrl>
|
||||
<dependencies>
|
||||
<dependency id="python3" version="3.6" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="tools\**" target="tools" />
|
||||
</files>
|
||||
</package>
|
@ -0,0 +1,6 @@
|
||||
$ErrorActionPreference = 'Stop';
|
||||
$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
|
||||
$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
|
@ -0,0 +1,2 @@
|
||||
$ErrorActionPreference = 'Stop';
|
||||
py -m pip uninstall -y $env:ChocolateyPackageName --disable-pip-version-check
|
@ -1,11 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
_http_complete() {
|
||||
local cur_word=${COMP_WORDS[COMP_CWORD]}
|
||||
local prev_word=${COMP_WORDS[COMP_CWORD - 1]}
|
||||
|
||||
if [[ "$cur_word" == -* ]]; then
|
||||
if [[ "$cur_word" == -* ]]; then
|
||||
_http_complete_options "$cur_word"
|
||||
fi
|
||||
}
|
||||
@ -18,6 +15,6 @@ _http_complete_options() {
|
||||
-v --verbose -h --headers -b --body -S --stream -o --output -d --download
|
||||
-c --continue --session --session-read-only -a --auth --auth-type --proxy
|
||||
--follow --verify --cert --cert-key --timeout --check-status --ignore-stdin
|
||||
--help --version --traceback --debug"
|
||||
--help --version --traceback --debug --raw"
|
||||
COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) )
|
||||
}
|
||||
|
@ -1,59 +1,137 @@
|
||||
function __fish_httpie_auth_types
|
||||
echo "basic"\t"Basic HTTP auth"
|
||||
echo "digest"\t"Digest HTTP auth"
|
||||
end
|
||||
|
||||
function __fish_httpie_styles
|
||||
echo "autumn"
|
||||
echo "borland"
|
||||
echo "bw"
|
||||
echo "colorful"
|
||||
echo "default"
|
||||
echo "emacs"
|
||||
echo "friendly"
|
||||
echo "fruity"
|
||||
echo "igor"
|
||||
echo "manni"
|
||||
echo "monokai"
|
||||
echo "murphy"
|
||||
echo "native"
|
||||
echo "paraiso-dark"
|
||||
echo "paraiso-light"
|
||||
echo "pastie"
|
||||
echo "perldoc"
|
||||
echo "rrt"
|
||||
echo "solarized"
|
||||
echo "tango"
|
||||
echo "trac"
|
||||
echo "vim"
|
||||
echo "vs"
|
||||
echo "xcode"
|
||||
echo "
|
||||
abap
|
||||
algol
|
||||
algol_nu
|
||||
arduino
|
||||
auto
|
||||
autumn
|
||||
borland
|
||||
bw
|
||||
colorful
|
||||
default
|
||||
emacs
|
||||
friendly
|
||||
fruity
|
||||
gruvbox-dark
|
||||
gruvbox-light
|
||||
igor
|
||||
inkpot
|
||||
lovelace
|
||||
manni
|
||||
material
|
||||
monokai
|
||||
murphy
|
||||
native
|
||||
paraiso-dark
|
||||
paraiso-light
|
||||
pastie
|
||||
perldoc
|
||||
rainbow_dash
|
||||
rrt
|
||||
sas
|
||||
solarized
|
||||
solarized-dark
|
||||
solarized-light
|
||||
stata
|
||||
stata-dark
|
||||
stata-light
|
||||
tango
|
||||
trac
|
||||
vim
|
||||
vs
|
||||
xcode
|
||||
zenburn"
|
||||
end
|
||||
|
||||
complete -x -c http -s s -l style -d 'Output coloring style (default is "monokai")' -A -a '(__fish_httpie_styles)'
|
||||
complete -c http -s f -l form -d 'Data items from the command line are serialized as form fields'
|
||||
complete -c http -s j -l json -d '(default) Data items from the command line are serialized as a JSON object'
|
||||
complete -x -c http -l pretty -d 'Controls output processing' -a "all colors format none" -A
|
||||
complete -x -c http -s p -l print -d 'String specifying what the output should contain'
|
||||
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
|
||||
complete -c http -s h -l headers -d 'Print only the response headers'
|
||||
complete -c http -s b -l body -d 'Print only the response body'
|
||||
complete -c http -s S -l stream -d 'Always stream the output by line'
|
||||
complete -c http -s o -l output -d 'Save output to FILE'
|
||||
complete -c http -s d -l download -d 'Do not print the response body to stdout'
|
||||
complete -c http -s c -l continue -d 'Resume an interrupted download'
|
||||
complete -x -c http -l session -d 'Create, or reuse and update a session'
|
||||
complete -x -c http -s a -l auth -d 'If only the username is provided (-a username), HTTPie will prompt for the password'
|
||||
complete -x -c http -l auth-type -d 'The authentication mechanism to be used' -a '(__fish_httpie_auth_types)' -A
|
||||
complete -x -c http -l proxy -d 'String mapping protocol to the URL of the proxy'
|
||||
complete -c http -l follow -d 'Allow full redirects'
|
||||
complete -x -c http -l verify -d 'SSL cert verification'
|
||||
complete -c http -l cert -d 'SSL cert'
|
||||
complete -c http -l cert-key -d 'Private SSL cert key'
|
||||
complete -x -c http -l timeout -d 'Connection timeout in seconds'
|
||||
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
|
||||
complete -c http -l ignore-stdin -d 'Do not attempt to read stdin'
|
||||
complete -c http -l help -d 'Show help'
|
||||
complete -c http -l version -d 'Show version'
|
||||
complete -c http -l traceback -d 'Prints exception traceback should one occur'
|
||||
complete -c http -l debug -d 'Show debugging information'
|
||||
function __fish_httpie_auth_types
|
||||
echo -e "basic\tBasic HTTP auth"
|
||||
echo -e "digest\tDigest HTTP auth"
|
||||
end
|
||||
|
||||
function __fish_http_verify_options
|
||||
echo -e "yes\tEnable cert verification"
|
||||
echo -e "no\tDisable cert verification"
|
||||
end
|
||||
|
||||
# Predefined Content Types
|
||||
|
||||
complete -c http -s j -l json -d 'Data items are serialized as a JSON object'
|
||||
complete -c http -s f -l form -d 'Data items are serialized as form fields'
|
||||
complete -c http -l multipart -d 'Always sends a multipart/form-data request'
|
||||
complete -c http -l boundary -x -d 'Custom boundary string for multipart/form-data requests'
|
||||
complete -c http -l raw -x -d 'Pass raw request data without extra processing'
|
||||
|
||||
|
||||
# Content Processing Options
|
||||
|
||||
complete -c http -s x -l compress -d 'Content compressed with Deflate algorithm'
|
||||
|
||||
|
||||
# Output Processing
|
||||
|
||||
complete -c http -l pretty -xa "all colors format none" -d 'Controls output processing'
|
||||
complete -c http -s s -l style -xa "(__fish_httpie_styles)" -d 'Output coloring style'
|
||||
complete -c http -l unsorted -d 'Disables all sorting while formatting output'
|
||||
complete -c http -l sorted -d 'Re-enables all sorting options while formatting output'
|
||||
complete -c http -l format-options -x -d 'Controls output formatting'
|
||||
|
||||
|
||||
# Output Options
|
||||
|
||||
complete -c http -s p -l print -x -d 'String specifying what the output should contain'
|
||||
complete -c http -s h -l headers -d 'Print only the response headers'
|
||||
complete -c http -s b -l body -d 'Print only the response body'
|
||||
complete -c http -s v -l verbose -d 'Print the whole request as well as the response'
|
||||
complete -c http -l all -d 'Show any intermediary requests/responses'
|
||||
complete -c http -s P -l history-print -x -d 'The same as --print but applies only to intermediary requests/responses'
|
||||
complete -c http -s S -l stream -d 'Always stream the response body by line'
|
||||
complete -c http -s o -l output -F -d 'Save output to FILE'
|
||||
complete -c http -s d -l download -d 'Download a file'
|
||||
complete -c http -s c -l continue -d 'Resume an interrupted download'
|
||||
complete -c http -s q -l quiet -d 'Do not print to stdout or stderr'
|
||||
|
||||
|
||||
# Sessions
|
||||
|
||||
complete -c http -l session -F -d 'Create, or reuse and update a session'
|
||||
complete -c http -l session-read-only -F -d 'Create or read a session without updating it'
|
||||
|
||||
|
||||
# Authentication
|
||||
|
||||
complete -c http -s a -l auth -x -d 'Username and password for authentication'
|
||||
complete -c http -s A -l auth-type -xa "(__fish_httpie_auth_types)" -d 'The authentication mechanism to be used'
|
||||
complete -c http -l ignore-netrc -d 'Ignore credentials from .netrc'
|
||||
|
||||
|
||||
# Network
|
||||
|
||||
complete -c http -l offline -d 'Build the request and print it but don\'t actually send it'
|
||||
complete -c http -l proxy -x -d 'String mapping protocol to the URL of the proxy'
|
||||
complete -c http -s F -l follow -d 'Follow 30x Location redirects'
|
||||
complete -c http -l max-redirects -x -d 'Set maximum number of redirects'
|
||||
complete -c http -l max-headers -x -d 'Maximum number of response headers to be read before giving up'
|
||||
complete -c http -l timeout -x -d 'Connection timeout in seconds'
|
||||
complete -c http -l check-status -d 'Error with non-200 HTTP status code'
|
||||
complete -c http -l path-as-is -d 'Bypass dot segment URL squashing'
|
||||
complete -c http -l chunked -d ''
|
||||
|
||||
|
||||
# SSL
|
||||
|
||||
complete -c http -l verify -xa "(__fish_http_verify_options)" -d 'Enable/disable cert verification'
|
||||
complete -c http -l ssl -x -d 'Desired protocol version to use'
|
||||
complete -c http -l ciphers -x -d 'String in the OpenSSL cipher list format'
|
||||
complete -c http -l cert -F -d 'Client side SSL certificate'
|
||||
complete -c http -l cert-key -F -d 'Private key to use with SSL'
|
||||
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
complete -c http -s I -l ignore-stdin -d 'Do not attempt to read stdin'
|
||||
complete -c http -l help -d 'Show help'
|
||||
complete -c http -l version -d 'Show version'
|
||||
complete -c http -l traceback -d 'Prints exception traceback should one occur'
|
||||
complete -c http -l default-scheme -x -d 'The default scheme to use'
|
||||
complete -c http -l debug -d 'Show debugging output'
|
||||
|
@ -1,69 +0,0 @@
|
||||
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
|
||||
# Only useful for testing until it gets accepted by homebrew maintainers.
|
||||
# (It will need to be updated from the repo version before next release.)
|
||||
#
|
||||
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
|
||||
#
|
||||
class Httpie < Formula
|
||||
include Language::Python::Virtualenv
|
||||
|
||||
desc "User-friendly cURL replacement (command-line HTTP client)"
|
||||
homepage "https://httpie.org/"
|
||||
url "https://files.pythonhosted.org/packages/44/ee/7177b743400d7f82a69bf30cb3c24ea4bb1f4aea68878bc540f732bf4940/httpie-1.0.0.tar.gz"
|
||||
sha256 "1650342d2eca2622092196bf106ab8f68ea2dbb2ed265d37191185618e159a25"
|
||||
head "https://github.com/jakubroztocil/httpie.git"
|
||||
|
||||
bottle do
|
||||
cellar :any_skip_relocation
|
||||
sha256 "7e9db255e324dd63b66106ca62ed7e4e81f6634c624dec3ff49c293aba1072a6" => :mojave
|
||||
sha256 "437504a11416284b17d3a801c267d0fd5e15416f38cff3abf7ed99b096b4828a" => :high_sierra
|
||||
sha256 "10b25fc787076719b1f1f9c242c5e9d872ebd1c7a6d83e6f1af983a17cd8ca55" => :sierra
|
||||
sha256 "1bd35480d1ef401bdad9c322e7c1624aefc9b5056530ab990e327d0bc397e4fb" => :el_capitan
|
||||
end
|
||||
|
||||
depends_on "python" ["3.6.5_1"]
|
||||
|
||||
resource "pygments" do
|
||||
url "https://files.pythonhosted.org/packages/71/2a/2e4e77803a8bd6408a2903340ac498cb0a2181811af7c9ec92cb70b0308a/Pygments-2.2.0.tar.gz"
|
||||
sha256 "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||
end
|
||||
|
||||
resource "requests" do
|
||||
url "https://files.pythonhosted.org/packages/97/10/92d25b93e9c266c94b76a5548f020f3f1dd0eb40649cb1993532c0af8f4c/requests-2.20.0.tar.gz"
|
||||
sha256 "99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c"
|
||||
end
|
||||
|
||||
resource "certifi" do
|
||||
url "https://files.pythonhosted.org/packages/41/b6/4f0cefba47656583217acd6cd797bc2db1fede0d53090fdc28ad2c8e0716/certifi-2018.10.15.tar.gz"
|
||||
sha256 "6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
|
||||
end
|
||||
|
||||
resource "urllib3" do
|
||||
url "https://files.pythonhosted.org/packages/a5/74/05ffd00b4b5c08306939c485869f5dc40cbc27357195b0a98b18e4c48893/urllib3-1.24.tar.gz"
|
||||
sha256 "41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae"
|
||||
end
|
||||
|
||||
resource "idna" do
|
||||
url "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz"
|
||||
sha256 "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
end
|
||||
|
||||
resource "chardet" do
|
||||
url "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz"
|
||||
sha256 "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
|
||||
end
|
||||
|
||||
resource "PySocks" do
|
||||
url "https://files.pythonhosted.org/packages/53/12/6bf1d764f128636cef7408e8156b7235b150ea31650d0260969215bb8e7d/PySocks-1.6.8.tar.gz"
|
||||
sha256 "3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672"
|
||||
end
|
||||
|
||||
def install
|
||||
virtualenv_install_with_resources
|
||||
end
|
||||
|
||||
test do
|
||||
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/httpie.rb"
|
||||
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
|
||||
end
|
||||
end
|
BIN
httpie.png
BIN
httpie.png
Binary file not shown.
Before Width: | Height: | Size: 681 KiB |
@ -1,32 +1,8 @@
|
||||
"""
|
||||
HTTPie - a CLI, cURL-like tool for humans.
|
||||
HTTPie: command-line HTTP client for the API era.
|
||||
|
||||
"""
|
||||
__version__ = '1.0.2'
|
||||
|
||||
__version__ = '2.6.0.dev0'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
||||
|
||||
class ExitStatus:
|
||||
"""Program exit code constants."""
|
||||
SUCCESS = 0
|
||||
ERROR = 1
|
||||
PLUGIN_ERROR = 7
|
||||
|
||||
# 128+2 SIGINT <http://www.tldp.org/LDP/abs/html/exitcodes.html>
|
||||
ERROR_CTRL_C = 130
|
||||
|
||||
ERROR_TIMEOUT = 2
|
||||
ERROR_TOO_MANY_REDIRECTS = 6
|
||||
|
||||
# Used only when requested with --check-status:
|
||||
ERROR_HTTP_3XX = 3
|
||||
ERROR_HTTP_4XX = 4
|
||||
ERROR_HTTP_5XX = 5
|
||||
|
||||
|
||||
EXIT_STATUS_LABELS = {
|
||||
value: key
|
||||
for key, value in ExitStatus.__dict__.items()
|
||||
if key.isupper()
|
||||
}
|
||||
|
@ -1,18 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
"""The main entry point. Invoke as `http' or `python -m httpie'.
|
||||
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
from .core import main
|
||||
sys.exit(main())
|
||||
from httpie.core import main
|
||||
exit_status = main()
|
||||
except KeyboardInterrupt:
|
||||
from . import ExitStatus
|
||||
sys.exit(ExitStatus.ERROR_CTRL_C)
|
||||
from httpie.status import ExitStatus
|
||||
exit_status = ExitStatus.ERROR_CTRL_C
|
||||
|
||||
return exit_status.value
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
if __name__ == '__main__': # pragma: nocover
|
||||
import sys
|
||||
sys.exit(main())
|
||||
|
0
httpie/cli/__init__.py
Normal file
0
httpie/cli/__init__.py
Normal file
466
httpie/cli/argparser.py
Normal file
466
httpie/cli/argparser.py
Normal file
@ -0,0 +1,466 @@
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from argparse import RawDescriptionHelpFormatter
|
||||
from textwrap import dedent
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from requests.utils import get_netrc_auth
|
||||
|
||||
from .argtypes import (
|
||||
AuthCredentials, KeyValueArgType, PARSED_DEFAULT_FORMAT_OPTIONS,
|
||||
parse_auth,
|
||||
parse_format_options,
|
||||
)
|
||||
from .constants import (
|
||||
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
|
||||
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
|
||||
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
|
||||
SEPARATOR_CREDENTIALS,
|
||||
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_GROUP_DATA_ITEMS, URL_SCHEME_RE,
|
||||
)
|
||||
from .exceptions import ParseError
|
||||
from .requestitems import RequestItems
|
||||
from ..context import Environment
|
||||
from ..plugins.registry import plugin_manager
|
||||
from ..utils import ExplicitNullAuth, get_content_type
|
||||
|
||||
|
||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
"""A nicer help formatter.
|
||||
|
||||
Help for arguments can be indented and contain new lines.
|
||||
It will be de-dented and arguments in the help
|
||||
will be separated by a blank line for better readability.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, max_help_position=6, *args, **kwargs):
|
||||
# A smaller indent for args help.
|
||||
kwargs['max_help_position'] = max_help_position
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _split_lines(self, text, width):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
|
||||
|
||||
# TODO: refactor and design type-annotated data structures
|
||||
# for raw args + parsed args and keep things immutable.
|
||||
class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||
and performs extra validation.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
|
||||
kwargs['add_help'] = False
|
||||
super().__init__(*args, formatter_class=formatter_class, **kwargs)
|
||||
self.env = None
|
||||
self.args = None
|
||||
self.has_stdin_data = False
|
||||
self.has_input_data = False
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(
|
||||
self,
|
||||
env: Environment,
|
||||
args=None,
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.args, no_options = super().parse_known_args(args, namespace)
|
||||
if self.args.prompt:
|
||||
return self.args
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
self.has_stdin_data = (
|
||||
self.env.stdin
|
||||
and not self.args.ignore_stdin
|
||||
and not self.env.stdin_isatty
|
||||
)
|
||||
self.has_input_data = self.has_stdin_data or self.args.raw is not None
|
||||
# Arguments processing and environment setup.
|
||||
self._apply_no_options(no_options)
|
||||
self._process_request_type()
|
||||
self._process_download_options()
|
||||
self._setup_standard_streams()
|
||||
self._process_output_options()
|
||||
self._process_pretty_options()
|
||||
self._process_format_options()
|
||||
self._guess_method()
|
||||
self._parse_items()
|
||||
self._process_url()
|
||||
self._process_auth()
|
||||
|
||||
if self.args.raw is not None:
|
||||
self._body_from_input(self.args.raw)
|
||||
elif self.has_stdin_data:
|
||||
self._body_from_file(self.env.stdin)
|
||||
|
||||
if self.args.compress:
|
||||
# TODO: allow --compress with --chunked / --multipart
|
||||
if self.args.chunked:
|
||||
self.error('cannot combine --compress and --chunked')
|
||||
if self.args.multipart:
|
||||
self.error('cannot combine --compress and --multipart')
|
||||
|
||||
return self.args
|
||||
|
||||
def _process_request_type(self):
|
||||
request_type = self.args.request_type
|
||||
self.args.json = request_type is RequestType.JSON
|
||||
self.args.multipart = request_type is RequestType.MULTIPART
|
||||
self.args.form = request_type in {
|
||||
RequestType.FORM,
|
||||
RequestType.MULTIPART,
|
||||
}
|
||||
|
||||
def _process_url(self):
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
if os.path.basename(self.env.program_name) == 'https':
|
||||
scheme = 'https://'
|
||||
else:
|
||||
scheme = self.args.default_scheme + '://'
|
||||
|
||||
# See if we're using curl style shorthand for localhost (:3000/foo)
|
||||
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
|
||||
if shorthand:
|
||||
port = shorthand.group(1)
|
||||
rest = shorthand.group(2)
|
||||
self.args.url = scheme + 'localhost'
|
||||
if port:
|
||||
self.args.url += ':' + port
|
||||
self.args.url += rest
|
||||
else:
|
||||
self.args.url = scheme + self.args.url
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _print_message(self, message, file=None):
|
||||
# Sneak in our stderr/stdout.
|
||||
file = {
|
||||
sys.stdout: self.env.stdout,
|
||||
sys.stderr: self.env.stderr,
|
||||
None: self.env.stderr
|
||||
}.get(file, file)
|
||||
if not hasattr(file, 'buffer') and isinstance(message, str):
|
||||
message = message.encode(self.env.stdout_encoding)
|
||||
super()._print_message(message, file)
|
||||
|
||||
def _setup_standard_streams(self):
|
||||
"""
|
||||
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
||||
|
||||
"""
|
||||
|
||||
self.args.output_file_specified = bool(self.args.output_file)
|
||||
if self.args.download:
|
||||
# FIXME: Come up with a cleaner solution.
|
||||
if not self.args.output_file and not self.env.stdout_isatty:
|
||||
# Use stdout as the download output file.
|
||||
self.args.output_file = self.env.stdout
|
||||
# With `--download`, we write everything that would normally go to
|
||||
# `stdout` to `stderr` instead. Let's replace the stream so that
|
||||
# we don't have to use many `if`s throughout the codebase.
|
||||
# The response body will be treated separately.
|
||||
self.env.stdout = self.env.stderr
|
||||
self.env.stdout_isatty = self.env.stderr_isatty
|
||||
|
||||
elif self.args.output_file:
|
||||
# When not `--download`ing, then `--output` simply replaces
|
||||
# `stdout`. The file is opened for appending, which isn't what
|
||||
# we want in this case.
|
||||
self.args.output_file.seek(0)
|
||||
try:
|
||||
self.args.output_file.truncate()
|
||||
except OSError as e:
|
||||
if e.errno == errno.EINVAL:
|
||||
# E.g. /dev/null on Linux.
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
self.env.stdout = self.args.output_file
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
if self.args.quiet:
|
||||
self.env.stderr = self.env.devnull
|
||||
if not (self.args.output_file_specified and not self.args.download):
|
||||
self.env.stdout = self.env.devnull
|
||||
|
||||
def _process_auth(self):
|
||||
# TODO: refactor & simplify this method.
|
||||
self.args.auth_plugin = None
|
||||
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
|
||||
auth_type_set = self.args.auth_type is not None
|
||||
url = urlsplit(self.args.url)
|
||||
|
||||
if self.args.auth is None and not auth_type_set:
|
||||
if url.username is not None:
|
||||
# Handle http://username:password@hostname/
|
||||
username = url.username
|
||||
password = url.password or ''
|
||||
self.args.auth = AuthCredentials(
|
||||
key=username,
|
||||
value=password,
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=SEPARATOR_CREDENTIALS.join([username, password])
|
||||
)
|
||||
|
||||
if self.args.auth is not None or auth_type_set:
|
||||
if not self.args.auth_type:
|
||||
self.args.auth_type = default_auth_plugin.auth_type
|
||||
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
|
||||
|
||||
if (not self.args.ignore_netrc
|
||||
and self.args.auth is None
|
||||
and plugin.netrc_parse):
|
||||
# Only host needed, so it’s OK URL not finalized.
|
||||
netrc_credentials = get_netrc_auth(self.args.url)
|
||||
if netrc_credentials:
|
||||
self.args.auth = AuthCredentials(
|
||||
key=netrc_credentials[0],
|
||||
value=netrc_credentials[1],
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=SEPARATOR_CREDENTIALS.join(netrc_credentials)
|
||||
)
|
||||
|
||||
if plugin.auth_require and self.args.auth is None:
|
||||
self.error('--auth required')
|
||||
|
||||
plugin.raw_auth = self.args.auth
|
||||
self.args.auth_plugin = plugin
|
||||
already_parsed = isinstance(self.args.auth, AuthCredentials)
|
||||
|
||||
if self.args.auth is None or not plugin.auth_parse:
|
||||
self.args.auth = plugin.get_auth()
|
||||
else:
|
||||
if already_parsed:
|
||||
# from the URL
|
||||
credentials = self.args.auth
|
||||
else:
|
||||
credentials = parse_auth(self.args.auth)
|
||||
|
||||
if (not credentials.has_password()
|
||||
and plugin.prompt_password):
|
||||
if self.args.ignore_stdin:
|
||||
# Non-tty stdin read by now
|
||||
self.error(
|
||||
'Unable to prompt for passwords because'
|
||||
' --ignore-stdin is set.'
|
||||
)
|
||||
credentials.prompt_password(url.netloc)
|
||||
self.args.auth = plugin.get_auth(
|
||||
username=credentials.key,
|
||||
password=credentials.value,
|
||||
)
|
||||
if not self.args.auth and self.args.ignore_netrc:
|
||||
# Set a no-op auth to force requests to ignore .netrc
|
||||
# <https://github.com/psf/requests/issues/2773#issuecomment-174312831>
|
||||
self.args.auth = ExplicitNullAuth()
|
||||
|
||||
def _apply_no_options(self, no_options):
|
||||
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||
its default value. This allows for un-setting of options, e.g.,
|
||||
specified in config.
|
||||
|
||||
"""
|
||||
invalid = []
|
||||
|
||||
for option in no_options:
|
||||
if not option.startswith('--no-'):
|
||||
invalid.append(option)
|
||||
continue
|
||||
|
||||
# --no-option => --option
|
||||
inverted = '--' + option[5:]
|
||||
for action in self._actions:
|
||||
if inverted in action.option_strings:
|
||||
setattr(self.args, action.dest, action.default)
|
||||
break
|
||||
else:
|
||||
invalid.append(option)
|
||||
|
||||
if invalid:
|
||||
self.error(f'unrecognized arguments: {" ".join(invalid)}')
|
||||
|
||||
def _body_from_file(self, fd):
|
||||
"""Read the data from a file-like object.
|
||||
|
||||
Bytes are always read.
|
||||
|
||||
"""
|
||||
self._ensure_one_data_source(self.args.data, self.args.files)
|
||||
self.args.data = getattr(fd, 'buffer', fd)
|
||||
|
||||
def _body_from_input(self, data):
|
||||
"""Read the data from the CLI.
|
||||
|
||||
"""
|
||||
self._ensure_one_data_source(self.has_stdin_data, self.args.data,
|
||||
self.args.files)
|
||||
self.args.data = data.encode()
|
||||
|
||||
def _ensure_one_data_source(self, *other_sources):
|
||||
"""There can only be one source of input request data.
|
||||
|
||||
"""
|
||||
if any(other_sources):
|
||||
self.error('Request body (from stdin, --raw or a file) and request '
|
||||
'data (key=value) cannot be mixed. Pass '
|
||||
'--ignore-stdin to let key/value take priority. '
|
||||
'See https://httpie.io/docs#scripting for details.')
|
||||
|
||||
def _guess_method(self):
|
||||
"""Set `args.method` if not specified to either POST or GET
|
||||
based on whether the request has data or not.
|
||||
|
||||
"""
|
||||
if self.args.method is None:
|
||||
# Invoked as `http URL'.
|
||||
assert not self.args.request_items
|
||||
if self.has_input_data:
|
||||
self.args.method = HTTP_POST
|
||||
else:
|
||||
self.args.method = HTTP_GET
|
||||
|
||||
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
|
||||
elif not re.match('^[a-zA-Z]+$', self.args.method):
|
||||
# Invoked as `http URL item+'. The URL is now in `args.method`
|
||||
# and the first ITEM is now incorrectly in `args.url`.
|
||||
try:
|
||||
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
||||
self.args.request_items.insert(0, KeyValueArgType(
|
||||
*SEPARATOR_GROUP_ALL_ITEMS).__call__(self.args.url))
|
||||
|
||||
except argparse.ArgumentTypeError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
|
||||
else:
|
||||
# Set the URL correctly
|
||||
self.args.url = self.args.method
|
||||
# Infer the method
|
||||
has_data = (
|
||||
self.has_input_data
|
||||
or any(
|
||||
item.sep in SEPARATOR_GROUP_DATA_ITEMS
|
||||
for item in self.args.request_items)
|
||||
)
|
||||
self.args.method = HTTP_POST if has_data else HTTP_GET
|
||||
|
||||
def _parse_items(self):
|
||||
"""
|
||||
Parse `args.request_items` into `args.headers`, `args.data`,
|
||||
`args.params`, and `args.files`.
|
||||
|
||||
"""
|
||||
try:
|
||||
request_items = RequestItems.from_args(
|
||||
request_item_args=self.args.request_items,
|
||||
as_form=self.args.form,
|
||||
)
|
||||
except ParseError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
else:
|
||||
self.args.headers = request_items.headers
|
||||
self.args.data = request_items.data
|
||||
self.args.files = request_items.files
|
||||
self.args.params = request_items.params
|
||||
self.args.multipart_data = request_items.multipart_data
|
||||
|
||||
if self.args.files and not self.args.form:
|
||||
# `http url @/path/to/file`
|
||||
request_file = None
|
||||
for key, file in self.args.files.items():
|
||||
if key != '':
|
||||
self.error(
|
||||
'Invalid file fields (perhaps you meant --form?):'
|
||||
f' {",".join(self.args.files.keys())}')
|
||||
if request_file is not None:
|
||||
self.error("Can't read request from multiple files")
|
||||
request_file = file
|
||||
|
||||
fn, fd, ct = request_file
|
||||
self.args.files = {}
|
||||
|
||||
self._body_from_file(fd)
|
||||
|
||||
if 'Content-Type' not in self.args.headers:
|
||||
content_type = get_content_type(fn)
|
||||
if content_type:
|
||||
self.args.headers['Content-Type'] = content_type
|
||||
|
||||
def _process_output_options(self):
|
||||
"""Apply defaults to output options, or validate the provided ones.
|
||||
|
||||
The default output options are stdout-type-sensitive.
|
||||
|
||||
"""
|
||||
|
||||
def check_options(value, option):
|
||||
unknown = set(value) - OUTPUT_OPTIONS
|
||||
if unknown:
|
||||
self.error(f'Unknown output options: {option}={",".join(unknown)}')
|
||||
|
||||
if self.args.verbose:
|
||||
self.args.all = True
|
||||
|
||||
if self.args.output_options is None:
|
||||
if self.args.verbose:
|
||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||
elif self.args.offline:
|
||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
|
||||
elif not self.env.stdout_isatty:
|
||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
|
||||
else:
|
||||
self.args.output_options = OUTPUT_OPTIONS_DEFAULT
|
||||
|
||||
if self.args.output_options_history is None:
|
||||
self.args.output_options_history = self.args.output_options
|
||||
|
||||
check_options(self.args.output_options, '--print')
|
||||
check_options(self.args.output_options_history, '--history-print')
|
||||
|
||||
if self.args.download and OUT_RESP_BODY in self.args.output_options:
|
||||
# Response body is always downloaded with --download and it goes
|
||||
# through a different routine, so we remove it.
|
||||
self.args.output_options = str(
|
||||
set(self.args.output_options) - set(OUT_RESP_BODY))
|
||||
|
||||
def _process_pretty_options(self):
|
||||
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
|
||||
self.args.prettify = PRETTY_MAP[
|
||||
'all' if self.env.stdout_isatty else 'none']
|
||||
elif (self.args.prettify and self.env.is_windows
|
||||
and self.args.output_file):
|
||||
self.error('Only terminal output can be colorized on Windows.')
|
||||
else:
|
||||
# noinspection PyTypeChecker
|
||||
self.args.prettify = PRETTY_MAP[self.args.prettify]
|
||||
|
||||
def _process_download_options(self):
|
||||
if self.args.offline:
|
||||
self.args.download = False
|
||||
self.args.download_resume = False
|
||||
return
|
||||
if not self.args.download:
|
||||
if self.args.download_resume:
|
||||
self.error('--continue only works with --download')
|
||||
if self.args.download_resume and not (
|
||||
self.args.download and self.args.output_file):
|
||||
self.error('--continue requires --output to be specified')
|
||||
|
||||
def _process_format_options(self):
|
||||
format_options = self.args.format_options or []
|
||||
parsed_options = PARSED_DEFAULT_FORMAT_OPTIONS
|
||||
for options_group in format_options:
|
||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||
self.args.format_options = parsed_options
|
260
httpie/cli/argtypes.py
Normal file
260
httpie/cli/argtypes.py
Normal file
@ -0,0 +1,260 @@
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from .constants import DEFAULT_FORMAT_OPTIONS, SEPARATOR_CREDENTIALS
|
||||
from ..sessions import VALID_SESSION_NAME_PATTERN
|
||||
|
||||
|
||||
class KeyValueArg:
|
||||
"""Base key-value pair parsed from CLI."""
|
||||
|
||||
def __init__(self, key: str, value: Optional[str], sep: str, orig: str):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.sep = sep
|
||||
self.orig = orig
|
||||
|
||||
def __eq__(self, other: 'KeyValueArg'):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
|
||||
class SessionNameValidator:
|
||||
|
||||
def __init__(self, error_message: str):
|
||||
self.error_message = error_message
|
||||
|
||||
def __call__(self, value: str) -> str:
|
||||
# Session name can be a path or just a name.
|
||||
if (os.path.sep not in value
|
||||
and not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||
raise argparse.ArgumentError(None, self.error_message)
|
||||
return value
|
||||
|
||||
|
||||
class Escaped(str):
|
||||
"""Represents an escaped character."""
|
||||
|
||||
def __repr__(self):
|
||||
return f"Escaped({repr(str(self))})"
|
||||
|
||||
|
||||
class KeyValueArgType:
|
||||
"""A key-value pair argument type used with `argparse`.
|
||||
|
||||
Parses a key-value arg and constructs a `KeyValueArg` instance.
|
||||
Used for headers, form data, and other key-value pair types.
|
||||
|
||||
"""
|
||||
|
||||
key_value_class = KeyValueArg
|
||||
|
||||
def __init__(self, *separators: str):
|
||||
self.separators = separators
|
||||
self.special_characters = set('\\')
|
||||
for separator in separators:
|
||||
self.special_characters.update(separator)
|
||||
|
||||
def __call__(self, s: str) -> KeyValueArg:
|
||||
"""Parse raw string arg and return `self.key_value_class` instance.
|
||||
|
||||
The best of `self.separators` is determined (first found, longest).
|
||||
Back slash escaped characters aren't considered as separators
|
||||
(or parts thereof). Literal back slash characters have to be escaped
|
||||
as well (r'\\').
|
||||
|
||||
"""
|
||||
tokens = self.tokenize(s)
|
||||
|
||||
# Sorting by length ensures that the longest one will be
|
||||
# chosen as it will overwrite any shorter ones starting
|
||||
# at the same position in the `found` dictionary.
|
||||
separators = sorted(self.separators, key=len)
|
||||
|
||||
for i, token in enumerate(tokens):
|
||||
|
||||
if isinstance(token, Escaped):
|
||||
continue
|
||||
|
||||
found = {}
|
||||
for sep in separators:
|
||||
pos = token.find(sep)
|
||||
if pos != -1:
|
||||
found[pos] = sep
|
||||
|
||||
if found:
|
||||
# Starting first, longest separator found.
|
||||
sep = found[min(found.keys())]
|
||||
|
||||
key, value = token.split(sep, 1)
|
||||
|
||||
# Any preceding tokens are part of the key.
|
||||
key = ''.join(tokens[:i]) + key
|
||||
|
||||
# Any following tokens are part of the value.
|
||||
value += ''.join(tokens[i + 1:])
|
||||
|
||||
break
|
||||
|
||||
else:
|
||||
raise argparse.ArgumentTypeError(f'{s!r} is not a valid value')
|
||||
|
||||
return self.key_value_class(key=key, value=value, sep=sep, orig=s)
|
||||
|
||||
def tokenize(self, s: str) -> List[Union[str, Escaped]]:
|
||||
r"""Tokenize the raw arg string
|
||||
|
||||
There are only two token types - strings and escaped characters:
|
||||
|
||||
>>> KeyValueArgType('=').tokenize(r'foo\=bar\\baz')
|
||||
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
|
||||
|
||||
"""
|
||||
tokens = ['']
|
||||
characters = iter(s)
|
||||
for char in characters:
|
||||
if char == '\\':
|
||||
char = next(characters, '')
|
||||
if char not in self.special_characters:
|
||||
tokens[-1] += '\\' + char
|
||||
else:
|
||||
tokens.extend([Escaped(char), ''])
|
||||
else:
|
||||
tokens[-1] += char
|
||||
return tokens
|
||||
|
||||
|
||||
class AuthCredentials(KeyValueArg):
|
||||
"""Represents parsed credentials."""
|
||||
|
||||
def has_password(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
def prompt_password(self, host: str):
|
||||
prompt_text = f'http: password for {self.key}@{host}: '
|
||||
try:
|
||||
self.value = self._getpass(prompt_text)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(0)
|
||||
|
||||
@staticmethod
|
||||
def _getpass(prompt):
|
||||
# To allow easy mocking.
|
||||
return getpass.getpass(str(prompt))
|
||||
|
||||
|
||||
class AuthCredentialsArgType(KeyValueArgType):
|
||||
"""A key-value arg type that parses credentials."""
|
||||
|
||||
key_value_class = AuthCredentials
|
||||
|
||||
def __call__(self, s):
|
||||
"""Parse credentials from `s`.
|
||||
|
||||
("username" or "username:password").
|
||||
|
||||
"""
|
||||
try:
|
||||
return super().__call__(s)
|
||||
except argparse.ArgumentTypeError:
|
||||
# No password provided, will prompt for it later.
|
||||
return self.key_value_class(
|
||||
key=s,
|
||||
value=None,
|
||||
sep=SEPARATOR_CREDENTIALS,
|
||||
orig=s
|
||||
)
|
||||
|
||||
|
||||
parse_auth = AuthCredentialsArgType(SEPARATOR_CREDENTIALS)
|
||||
|
||||
|
||||
def readable_file_arg(filename):
|
||||
try:
|
||||
with open(filename, 'rb'):
|
||||
return filename
|
||||
except OSError as ex:
|
||||
raise argparse.ArgumentTypeError(f'{ex.filename}: {ex.strerror}')
|
||||
|
||||
|
||||
def parse_format_options(s: str, defaults: Optional[dict]) -> dict:
|
||||
"""
|
||||
Parse `s` and update `defaults` with the parsed values.
|
||||
|
||||
>>> parse_format_options(
|
||||
... defaults={'json': {'indent': 4, 'sort_keys': True}},
|
||||
... s='json.indent:2,json.sort_keys:False',
|
||||
... )
|
||||
{'json': {'indent': 2, 'sort_keys': False}}
|
||||
|
||||
"""
|
||||
value_map = {
|
||||
'true': True,
|
||||
'false': False,
|
||||
}
|
||||
options = deepcopy(defaults or {})
|
||||
for option in s.split(','):
|
||||
try:
|
||||
path, value = option.lower().split(':')
|
||||
section, key = path.split('.')
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(f'invalid option {option!r}')
|
||||
|
||||
if value in value_map:
|
||||
parsed_value = value_map[value]
|
||||
else:
|
||||
if value.isnumeric():
|
||||
parsed_value = int(value)
|
||||
else:
|
||||
parsed_value = value
|
||||
|
||||
if defaults is None:
|
||||
options.setdefault(section, {})
|
||||
else:
|
||||
try:
|
||||
default_value = defaults[section][key]
|
||||
except KeyError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'invalid key {path!r}')
|
||||
|
||||
default_type, parsed_type = type(default_value), type(parsed_value)
|
||||
if parsed_type is not default_type:
|
||||
raise argparse.ArgumentTypeError(
|
||||
'invalid value'
|
||||
f' {value!r} in {option!r}'
|
||||
f' (expected {default_type.__name__}'
|
||||
f' got {parsed_type.__name__})'
|
||||
)
|
||||
|
||||
options[section][key] = parsed_value
|
||||
|
||||
return options
|
||||
|
||||
|
||||
PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
|
||||
s=','.join(DEFAULT_FORMAT_OPTIONS),
|
||||
defaults=None,
|
||||
)
|
||||
|
||||
|
||||
def response_charset_type(encoding: str) -> str:
|
||||
try:
|
||||
''.encode(encoding)
|
||||
except LookupError:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'{encoding!r} is not a supported encoding')
|
||||
return encoding
|
||||
|
||||
|
||||
def response_mime_type(mime_type: str) -> str:
|
||||
if mime_type.count('/') != 1:
|
||||
raise argparse.ArgumentTypeError(
|
||||
f'{mime_type!r} doesn’t look like a mime type; use type/subtype')
|
||||
return mime_type
|
113
httpie/cli/constants.py
Normal file
113
httpie/cli/constants.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Parsing and processing of CLI input (args, auth credentials, files, stdin).
|
||||
|
||||
"""
|
||||
import enum
|
||||
import re
|
||||
|
||||
|
||||
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
|
||||
# Various separators used in args
|
||||
SEPARATOR_HEADER = ':'
|
||||
SEPARATOR_HEADER_EMPTY = ';'
|
||||
SEPARATOR_CREDENTIALS = ':'
|
||||
SEPARATOR_PROXY = ':'
|
||||
SEPARATOR_DATA_STRING = '='
|
||||
SEPARATOR_DATA_RAW_JSON = ':='
|
||||
SEPARATOR_FILE_UPLOAD = '@'
|
||||
SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEPARATOR_QUERY_PARAM = '=='
|
||||
|
||||
# Separators that become request data
|
||||
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE
|
||||
})
|
||||
|
||||
SEPARATORS_GROUP_MULTIPART = frozenset({
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
})
|
||||
|
||||
# Separators for items whose value is a filename to be embedded
|
||||
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
})
|
||||
|
||||
# Separators for raw JSON items
|
||||
SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
# Separators allowed in ITEM arguments
|
||||
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
|
||||
SEPARATOR_HEADER,
|
||||
SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
SEPARATOR_DATA_STRING,
|
||||
SEPARATOR_DATA_RAW_JSON,
|
||||
SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
})
|
||||
|
||||
# Output options
|
||||
OUT_REQ_HEAD = 'H'
|
||||
OUT_REQ_BODY = 'B'
|
||||
OUT_RESP_HEAD = 'h'
|
||||
OUT_RESP_BODY = 'b'
|
||||
|
||||
OUTPUT_OPTIONS = frozenset({
|
||||
OUT_REQ_HEAD,
|
||||
OUT_REQ_BODY,
|
||||
OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY
|
||||
})
|
||||
|
||||
# Pretty
|
||||
PRETTY_MAP = {
|
||||
'all': ['format', 'colors'],
|
||||
'colors': ['colors'],
|
||||
'format': ['format'],
|
||||
'none': []
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
|
||||
DEFAULT_FORMAT_OPTIONS = [
|
||||
'headers.sort:true',
|
||||
'json.format:true',
|
||||
'json.indent:4',
|
||||
'json.sort_keys:true',
|
||||
'xml.format:true',
|
||||
'xml.indent:2',
|
||||
]
|
||||
SORTED_FORMAT_OPTIONS = [
|
||||
'headers.sort:true',
|
||||
'json.sort_keys:true',
|
||||
]
|
||||
SORTED_FORMAT_OPTIONS_STRING = ','.join(SORTED_FORMAT_OPTIONS)
|
||||
UNSORTED_FORMAT_OPTIONS_STRING = ','.join(
|
||||
option.replace('true', 'false') for option in SORTED_FORMAT_OPTIONS)
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_BODY
|
||||
|
||||
|
||||
class RequestType(enum.Enum):
|
||||
FORM = enum.auto()
|
||||
MULTIPART = enum.auto()
|
||||
JSON = enum.auto()
|
@ -1,86 +1,65 @@
|
||||
"""CLI arguments definition.
|
||||
|
||||
NOTE: the CLI interface may change before reaching v1.0.
|
||||
"""
|
||||
CLI arguments definition.
|
||||
|
||||
"""
|
||||
# noinspection PyCompatibility
|
||||
from argparse import (
|
||||
RawDescriptionHelpFormatter, FileType,
|
||||
OPTIONAL, ZERO_OR_MORE, SUPPRESS
|
||||
)
|
||||
from argparse import FileType, OPTIONAL, SUPPRESS, ZERO_OR_MORE
|
||||
from textwrap import dedent, wrap
|
||||
|
||||
from httpie import __doc__, __version__
|
||||
from httpie.input import (
|
||||
HTTPieArgumentParser, KeyValueArgType,
|
||||
SEP_PROXY, SEP_GROUP_ALL_ITEMS,
|
||||
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
|
||||
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
|
||||
readable_file_arg, SSL_VERSION_ARG_MAPPING
|
||||
from .. import __doc__, __version__
|
||||
from .argparser import HTTPieArgumentParser
|
||||
from .argtypes import (
|
||||
KeyValueArgType, SessionNameValidator,
|
||||
readable_file_arg, response_charset_type, response_mime_type,
|
||||
)
|
||||
from httpie.output.formatters.colors import (
|
||||
AVAILABLE_STYLES, DEFAULT_STYLE, AUTO_STYLE
|
||||
from .constants import (
|
||||
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
|
||||
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
|
||||
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
|
||||
SORTED_FORMAT_OPTIONS_STRING,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING,
|
||||
)
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
||||
|
||||
|
||||
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
|
||||
"""A nicer help formatter.
|
||||
|
||||
Help for arguments can be indented and contain new lines.
|
||||
It will be de-dented and arguments in the help
|
||||
will be separated by a blank line for better readability.
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, max_help_position=6, *args, **kwargs):
|
||||
# A smaller indent for args help.
|
||||
kwargs['max_help_position'] = max_help_position
|
||||
super(HTTPieHelpFormatter, self).__init__(*args, **kwargs)
|
||||
|
||||
def _split_lines(self, text, width):
|
||||
text = dedent(text).strip() + '\n\n'
|
||||
return text.splitlines()
|
||||
from ..output.formatters.colors import (
|
||||
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
|
||||
)
|
||||
from ..plugins.builtin import BuiltinAuthPlugin
|
||||
from ..plugins.registry import plugin_manager
|
||||
from ..sessions import DEFAULT_SESSIONS_DIR
|
||||
from ..ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||
|
||||
|
||||
parser = HTTPieArgumentParser(
|
||||
prog='http',
|
||||
formatter_class=HTTPieHelpFormatter,
|
||||
description='%s <http://httpie.org>' % __doc__.strip(),
|
||||
epilog=dedent("""
|
||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||
epilog=dedent('''
|
||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
|
||||
https://github.com/jakubroztocil/httpie/issues
|
||||
https://github.com/httpie/httpie/issues
|
||||
|
||||
"""),
|
||||
'''),
|
||||
)
|
||||
|
||||
|
||||
#######################################################################
|
||||
# Positional arguments.
|
||||
#######################################################################
|
||||
|
||||
positional = parser.add_argument_group(
|
||||
title='Positional Arguments',
|
||||
description=dedent("""
|
||||
description=dedent('''
|
||||
These arguments come after any flags and in the order they are listed here.
|
||||
Only URL is required.
|
||||
|
||||
""")
|
||||
''')
|
||||
)
|
||||
positional.add_argument(
|
||||
'method',
|
||||
dest='method',
|
||||
metavar='METHOD',
|
||||
nargs=OPTIONAL,
|
||||
default=None,
|
||||
help="""
|
||||
help='''
|
||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||
|
||||
This argument can be omitted in which case HTTPie will use POST if there
|
||||
@ -89,12 +68,13 @@ positional.add_argument(
|
||||
$ http example.org # => GET
|
||||
$ http example.org hello=world # => POST
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
positional.add_argument(
|
||||
'url',
|
||||
dest='url',
|
||||
metavar='URL',
|
||||
help="""
|
||||
nargs=OPTIONAL,
|
||||
help='''
|
||||
The scheme defaults to 'http://' if the URL does not include one.
|
||||
(You can override this with: --default-scheme=https)
|
||||
|
||||
@ -103,21 +83,21 @@ positional.add_argument(
|
||||
$ http :3000 # => http://localhost:3000
|
||||
$ http :/foo # => http://localhost/foo
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
positional.add_argument(
|
||||
'items',
|
||||
dest='request_items',
|
||||
metavar='REQUEST_ITEM',
|
||||
nargs=ZERO_OR_MORE,
|
||||
default=None,
|
||||
type=KeyValueArgType(*SEP_GROUP_ALL_ITEMS),
|
||||
help=r"""
|
||||
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
||||
help=r'''
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
|
||||
':' HTTP headers:
|
||||
|
||||
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
|
||||
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||
|
||||
'==' URL parameters to be appended to the request URI:
|
||||
|
||||
@ -132,9 +112,10 @@ positional.add_argument(
|
||||
|
||||
awesome:=true amount:=42 colors:='["red", "green", "blue"]'
|
||||
|
||||
'@' Form file fields (only with --form, -f):
|
||||
'@' Form file fields (only with --form or --multipart):
|
||||
|
||||
cs@~/Documents/CV.pdf
|
||||
cv@~/Documents/CV.pdf
|
||||
cv@'~/Documents/CV.pdf;type=application/pdf'
|
||||
|
||||
'=@' A data field like '=', but takes a file path and embeds its content:
|
||||
|
||||
@ -148,10 +129,9 @@ positional.add_argument(
|
||||
|
||||
field-name-with\:colon=value
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
#######################################################################
|
||||
# Content type.
|
||||
#######################################################################
|
||||
@ -163,28 +143,93 @@ content_type = parser.add_argument_group(
|
||||
|
||||
content_type.add_argument(
|
||||
'--json', '-j',
|
||||
action='store_true',
|
||||
help="""
|
||||
action='store_const',
|
||||
const=RequestType.JSON,
|
||||
dest='request_type',
|
||||
help='''
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
(if not specified).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--form', '-f',
|
||||
action='store_true',
|
||||
help="""
|
||||
action='store_const',
|
||||
const=RequestType.FORM,
|
||||
dest='request_type',
|
||||
help='''
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||
specified). The presence of any file fields results in a
|
||||
multipart/form-data request.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--multipart',
|
||||
action='store_const',
|
||||
const=RequestType.MULTIPART,
|
||||
dest='request_type',
|
||||
help='''
|
||||
Similar to --form, but always sends a multipart/form-data
|
||||
request (i.e., even without files).
|
||||
|
||||
'''
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--boundary',
|
||||
help='''
|
||||
Specify a custom boundary string for multipart/form-data requests.
|
||||
Only has effect only together with --form.
|
||||
|
||||
'''
|
||||
)
|
||||
content_type.add_argument(
|
||||
'--raw',
|
||||
help='''
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
|
||||
$ http --raw='data' pie.dev/post
|
||||
|
||||
You can achieve the same by piping the data via stdin:
|
||||
|
||||
$ echo data | http pie.dev/post
|
||||
|
||||
Or have HTTPie load the raw data from a file:
|
||||
|
||||
$ http pie.dev/post @data.txt
|
||||
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
#######################################################################
|
||||
# Content processing.
|
||||
#######################################################################
|
||||
|
||||
content_processing = parser.add_argument_group(
|
||||
title='Content Processing Options',
|
||||
description=None
|
||||
)
|
||||
|
||||
content_processing.add_argument(
|
||||
'--compress', '-x',
|
||||
action='count',
|
||||
default=0,
|
||||
help='''
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
|
||||
Compression is skipped if it appears that compression ratio is
|
||||
negative. Compression can be forced by repeating the argument.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output processing
|
||||
#######################################################################
|
||||
@ -196,23 +241,23 @@ output_processing.add_argument(
|
||||
dest='prettify',
|
||||
default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
help="""
|
||||
help='''
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
and formatting (default for terminal output), "colors", or "format".
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_processing.add_argument(
|
||||
'--style', '-s',
|
||||
dest='style',
|
||||
metavar='STYLE',
|
||||
default=DEFAULT_STYLE,
|
||||
choices=AVAILABLE_STYLES,
|
||||
help="""
|
||||
Output coloring style (default is "{default}"). One of:
|
||||
choices=sorted(AVAILABLE_STYLES),
|
||||
help='''
|
||||
Output coloring style (default is "{default}"). It can be One of:
|
||||
|
||||
{available_styles}
|
||||
{available_styles}
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
|
||||
@ -220,16 +265,100 @@ output_processing.add_argument(
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
|
||||
""".format(
|
||||
'''.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
'{0}{1}'.format(8 * ' ', line.strip())
|
||||
f' {line.strip()}'
|
||||
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
|
||||
).rstrip(),
|
||||
).strip(),
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
)
|
||||
_sorted_kwargs = {
|
||||
'action': 'append_const',
|
||||
'const': SORTED_FORMAT_OPTIONS_STRING,
|
||||
'dest': 'format_options'
|
||||
}
|
||||
_unsorted_kwargs = {
|
||||
'action': 'append_const',
|
||||
'const': UNSORTED_FORMAT_OPTIONS_STRING,
|
||||
'dest': 'format_options'
|
||||
}
|
||||
# The closest approx. of the documented resetting to default via --no-<option>.
|
||||
# We hide them from the doc because they act only as low-level aliases here.
|
||||
output_processing.add_argument('--no-unsorted', **_sorted_kwargs, help=SUPPRESS)
|
||||
output_processing.add_argument('--no-sorted', **_unsorted_kwargs, help=SUPPRESS)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--unsorted',
|
||||
**_unsorted_kwargs,
|
||||
help=f'''
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
--format-options={UNSORTED_FORMAT_OPTIONS_STRING}
|
||||
|
||||
'''
|
||||
)
|
||||
output_processing.add_argument(
|
||||
'--sorted',
|
||||
**_sorted_kwargs,
|
||||
help=f'''
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
--format-options={SORTED_FORMAT_OPTIONS_STRING}
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--response-charset',
|
||||
metavar='ENCODING',
|
||||
type=response_charset_type,
|
||||
help='''
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
--response-charset=utf8
|
||||
--response-charset=big5
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--response-mime',
|
||||
metavar='MIME_TYPE',
|
||||
type=response_mime_type,
|
||||
help='''
|
||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||
|
||||
--response-mime=application/json
|
||||
--response-mime=text/xml
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
output_processing.add_argument(
|
||||
'--format-options',
|
||||
action='append',
|
||||
help='''
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) --pretty=all or --pretty=format.
|
||||
The following are the default options:
|
||||
|
||||
{option_list}
|
||||
|
||||
You may use this option multiple times, as well as specify multiple
|
||||
comma-separated options at the same time. For example, this modifies the
|
||||
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||
|
||||
--format-options json.sort_keys:false,json.indent:2
|
||||
|
||||
This is something you will typically put into your config file.
|
||||
|
||||
'''.format(
|
||||
option_list='\n'.join(
|
||||
f' {option}' for option in DEFAULT_FORMAT_OPTIONS).strip()
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Output options
|
||||
@ -240,93 +369,83 @@ output_options.add_argument(
|
||||
'--print', '-p',
|
||||
dest='output_options',
|
||||
metavar='WHAT',
|
||||
help="""
|
||||
help=f'''
|
||||
String specifying what the output should contain:
|
||||
|
||||
'{req_head}' request headers
|
||||
'{req_body}' request body
|
||||
'{res_head}' response headers
|
||||
'{res_body}' response body
|
||||
'{OUT_REQ_HEAD}' request headers
|
||||
'{OUT_REQ_BODY}' request body
|
||||
'{OUT_RESP_HEAD}' response headers
|
||||
'{OUT_RESP_BODY}' response body
|
||||
|
||||
The default behaviour is '{default}' (i.e., the response headers and body
|
||||
is printed), if standard output is not redirected. If the output is piped
|
||||
to another program or to a file, then only the response body is printed
|
||||
by default.
|
||||
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
|
||||
headers and body is printed), if standard output is not redirected.
|
||||
If the output is piped to another program or to a file, then only the
|
||||
response body is printed by default.
|
||||
|
||||
"""
|
||||
.format(
|
||||
req_head=OUT_REQ_HEAD,
|
||||
req_body=OUT_REQ_BODY,
|
||||
res_head=OUT_RESP_HEAD,
|
||||
res_body=OUT_RESP_BODY,
|
||||
default=OUTPUT_OPTIONS_DEFAULT,
|
||||
)
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--headers', '-h',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_HEAD,
|
||||
help="""
|
||||
Print only the response headers. Shortcut for --print={0}.
|
||||
help=f'''
|
||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||
|
||||
"""
|
||||
.format(OUT_RESP_HEAD)
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--body', '-b',
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_BODY,
|
||||
help="""
|
||||
Print only the response body. Shortcut for --print={0}.
|
||||
help=f'''
|
||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||
|
||||
"""
|
||||
.format(OUT_RESP_BODY)
|
||||
'''
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--verbose', '-v',
|
||||
dest='verbose',
|
||||
action='store_true',
|
||||
help="""
|
||||
help=f'''
|
||||
Verbose output. Print the whole request as well as the response. Also print
|
||||
any intermediary requests/responses (such as redirects).
|
||||
It's a shortcut for: --all --print={0}
|
||||
It's a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
||||
|
||||
"""
|
||||
.format(''.join(OUTPUT_OPTIONS))
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
followed redirects (with --follow), the first unauthorized request when
|
||||
Digest auth is used (--auth=digest), etc.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--history-print', '-P',
|
||||
dest='output_options_history',
|
||||
metavar='WHAT',
|
||||
help="""
|
||||
help='''
|
||||
The same as --print, -p but applies only to intermediary requests/responses
|
||||
(such as redirects) when their inclusion is enabled with --all. If this
|
||||
options is not specified, then they are formatted the same way as the final
|
||||
response.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--stream', '-S',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Always stream the output by line, i.e., behave like `tail -f'.
|
||||
help='''
|
||||
Always stream the response body by line, i.e., behave like `tail -f'.
|
||||
|
||||
Without --stream and with --pretty (either set or implied),
|
||||
HTTPie fetches the whole response before it outputs the processed data.
|
||||
@ -337,19 +456,19 @@ output_options.add_argument(
|
||||
It is useful also without --pretty: It ensures that the output is flushed
|
||||
more often and in smaller chunks.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--output', '-o',
|
||||
type=FileType('a+b'),
|
||||
dest='output_file',
|
||||
metavar='FILE',
|
||||
help="""
|
||||
help='''
|
||||
Save output to FILE instead of stdout. If --download is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
printed to stderr.
|
||||
|
||||
"""
|
||||
'''
|
||||
|
||||
)
|
||||
|
||||
@ -357,12 +476,12 @@ output_options.add_argument(
|
||||
'--download', '-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with --output
|
||||
[filename]. This action is similar to the default behaviour of wget.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
@ -370,20 +489,30 @@ output_options.add_argument(
|
||||
dest='download_resume',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Resume an interrupted download. Note that the --output option needs to be
|
||||
specified as well.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
output_options.add_argument(
|
||||
'--quiet', '-q',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
Do not print to stdout or stderr.
|
||||
stdout is still redirected if --output is specified.
|
||||
Flag doesn't affect behaviour of download beyond not printing to terminal.
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Sessions
|
||||
#######################################################################
|
||||
|
||||
sessions = parser.add_argument_group(title='Sessions')\
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
sessions = parser.add_argument_group(title='Sessions') \
|
||||
.add_mutually_exclusive_group(required=False)
|
||||
|
||||
session_name_validator = SessionNameValidator(
|
||||
'Session name contains invalid characters.'
|
||||
@ -393,27 +522,26 @@ sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help="""
|
||||
help=f'''
|
||||
Create, or reuse and update a session. Within a session, custom headers,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
Session files are stored in:
|
||||
|
||||
{session_dir}/<HOST>/<SESSION_NAME>.json.
|
||||
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
|
||||
|
||||
"""
|
||||
.format(session_dir=DEFAULT_SESSIONS_DIR)
|
||||
'''
|
||||
)
|
||||
sessions.add_argument(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
help="""
|
||||
help='''
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -426,15 +554,15 @@ auth.add_argument(
|
||||
'--auth', '-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS]',
|
||||
help="""
|
||||
help='''
|
||||
If only the username is provided (-a username), HTTPie will prompt
|
||||
for the password.
|
||||
|
||||
""",
|
||||
''',
|
||||
)
|
||||
|
||||
|
||||
class _AuthTypeLazyChoices(object):
|
||||
class _AuthTypeLazyChoices:
|
||||
# Needed for plugin testing
|
||||
|
||||
def __contains__(self, item):
|
||||
@ -449,19 +577,18 @@ auth.add_argument(
|
||||
'--auth-type', '-A',
|
||||
choices=_AuthTypeLazyChoices(),
|
||||
default=None,
|
||||
help="""
|
||||
help='''
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
|
||||
"""
|
||||
.format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'''.format(default=_auth_plugins[0].auth_type, types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
'' if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else ' (provided by %s)' % plugin.package_name
|
||||
else f' (provided by {plugin.package_name})'
|
||||
),
|
||||
description=(
|
||||
'' if not plugin.description else
|
||||
@ -471,7 +598,15 @@ auth.add_argument(
|
||||
for plugin in _auth_plugins
|
||||
)),
|
||||
)
|
||||
auth.add_argument(
|
||||
'--ignore-netrc',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='''
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
''',
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# Network
|
||||
@ -479,55 +614,79 @@ auth.add_argument(
|
||||
|
||||
network = parser.add_argument_group(title='Network')
|
||||
|
||||
network.add_argument(
|
||||
'--offline',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='''
|
||||
Build the request and print it but don’t actually send it.
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--proxy',
|
||||
default=[],
|
||||
action='append',
|
||||
metavar='PROTOCOL:PROXY_URL',
|
||||
type=KeyValueArgType(SEP_PROXY),
|
||||
help="""
|
||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||
help='''
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
different protocols.
|
||||
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||
and $HTTPS_proxy are supported as well.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--follow', '-F',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
Follow 30x Location redirects.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-redirects',
|
||||
type=int,
|
||||
default=30,
|
||||
help="""
|
||||
help='''
|
||||
By default, requests have a limit of 30 redirects (works with --follow).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-headers',
|
||||
type=int,
|
||||
default=0,
|
||||
help='''
|
||||
The maximum number of response headers to be read before giving up
|
||||
(default 0, i.e., no limit).
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--timeout',
|
||||
type=float,
|
||||
default=30,
|
||||
default=0,
|
||||
metavar='SECONDS',
|
||||
help="""
|
||||
The connection timeout of the request in seconds. The default value is
|
||||
30 seconds.
|
||||
help='''
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
This is not a time limit on the entire response download;
|
||||
rather, an error is reported if the server has not issued a response for
|
||||
timeout seconds (more precisely, if no bytes have been received on
|
||||
the underlying socket for timeout seconds).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--check-status',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
help='''
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
exit with an error if the status indicates one.
|
||||
@ -537,9 +696,28 @@ network.add_argument(
|
||||
3xx (Redirect) and --follow hasn't been set, then the exit status is 3.
|
||||
Also an error message is written to stderr if stdout is redirected.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
network.add_argument(
|
||||
'--path-as-is',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='''
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--chunked',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='''
|
||||
Enable streaming via chunked transfer encoding.
|
||||
The Transfer-Encoding header is set to chunked.
|
||||
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
# SSL
|
||||
@ -549,47 +727,58 @@ ssl = parser.add_argument_group(title='SSL')
|
||||
ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
help="""
|
||||
help='''
|
||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||
variable instead.)
|
||||
"""
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
|
||||
'--ssl',
|
||||
dest='ssl_version',
|
||||
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
|
||||
help="""
|
||||
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
|
||||
help='''
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
the server and your installation of OpenSSL support. Available protocols
|
||||
may vary depending on OpenSSL installation (only the supported ones
|
||||
are shown here).
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ciphers',
|
||||
help=f'''
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
is used:
|
||||
|
||||
{DEFAULT_SSL_CIPHERS}
|
||||
|
||||
'''
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--cert',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
help='''
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
specify --cert-key separately.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
ssl.add_argument(
|
||||
'--cert-key',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
help="""
|
||||
help='''
|
||||
The private key to use with SSL. Only needed if --cert is given and the
|
||||
certificate file does not contain the private key.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -602,53 +791,62 @@ troubleshooting.add_argument(
|
||||
'--ignore-stdin', '-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Do not attempt to read stdin.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help',
|
||||
default=SUPPRESS,
|
||||
help="""
|
||||
help='''
|
||||
Show this help message and exit.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help="""
|
||||
help='''
|
||||
Show version and exit.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--default-scheme',
|
||||
default="http",
|
||||
help="""
|
||||
help='''
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
help='''
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
||||
"""
|
||||
'''
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--prompt',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='''
|
||||
Start the shell!
|
||||
|
||||
'''
|
||||
)
|
58
httpie/cli/dicts.py
Normal file
58
httpie/cli/dicts.py
Normal file
@ -0,0 +1,58 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
|
||||
class RequestHeadersDict(CaseInsensitiveDict):
|
||||
"""
|
||||
Headers are case-insensitive and multiple values are currently not supported.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class RequestJSONDataDict(OrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class MultiValueOrderedDict(OrderedDict):
|
||||
"""Multi-value dict for URL parameters and form data."""
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""
|
||||
If `key` is assigned more than once, `self[key]` holds a
|
||||
`list` of all the values.
|
||||
|
||||
This allows having multiple fields with the same name in form
|
||||
data and URL params.
|
||||
|
||||
"""
|
||||
assert not isinstance(value, list)
|
||||
if key not in self:
|
||||
super().__setitem__(key, value)
|
||||
else:
|
||||
if not isinstance(self[key], list):
|
||||
super().__setitem__(key, [self[key]])
|
||||
self[key].append(value)
|
||||
|
||||
def items(self):
|
||||
for key, values in super().items():
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
class RequestQueryParamsDict(MultiValueOrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class RequestDataDict(MultiValueOrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class MultipartRequestDataDict(MultiValueOrderedDict):
|
||||
pass
|
||||
|
||||
|
||||
class RequestFilesDict(RequestDataDict):
|
||||
pass
|
2
httpie/cli/exceptions.py
Normal file
2
httpie/cli/exceptions.py
Normal file
@ -0,0 +1,2 @@
|
||||
class ParseError(Exception):
|
||||
pass
|
155
httpie/cli/requestitems.py
Normal file
155
httpie/cli/requestitems.py
Normal file
@ -0,0 +1,155 @@
|
||||
import os
|
||||
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
|
||||
|
||||
from .argtypes import KeyValueArg
|
||||
from .constants import (
|
||||
SEPARATORS_GROUP_MULTIPART, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
|
||||
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
|
||||
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
|
||||
SEPARATOR_QUERY_PARAM,
|
||||
)
|
||||
from .dicts import (
|
||||
MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
|
||||
RequestHeadersDict, RequestJSONDataDict,
|
||||
RequestQueryParamsDict,
|
||||
)
|
||||
from .exceptions import ParseError
|
||||
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys
|
||||
|
||||
|
||||
class RequestItems:
|
||||
|
||||
def __init__(self, as_form=False):
|
||||
self.headers = RequestHeadersDict()
|
||||
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
|
||||
self.files = RequestFilesDict()
|
||||
self.params = RequestQueryParamsDict()
|
||||
# To preserve the order of fields in file upload multipart requests.
|
||||
self.multipart_data = MultipartRequestDataDict()
|
||||
|
||||
@classmethod
|
||||
def from_args(
|
||||
cls,
|
||||
request_item_args: List[KeyValueArg],
|
||||
as_form=False,
|
||||
) -> 'RequestItems':
|
||||
instance = cls(as_form=as_form)
|
||||
rules: Dict[str, Tuple[Callable, dict]] = {
|
||||
SEPARATOR_HEADER: (
|
||||
process_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_HEADER_EMPTY: (
|
||||
process_empty_header_arg,
|
||||
instance.headers,
|
||||
),
|
||||
SEPARATOR_QUERY_PARAM: (
|
||||
process_query_param_arg,
|
||||
instance.params,
|
||||
),
|
||||
SEPARATOR_FILE_UPLOAD: (
|
||||
process_file_upload_arg,
|
||||
instance.files,
|
||||
),
|
||||
SEPARATOR_DATA_STRING: (
|
||||
process_data_item_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_FILE_CONTENTS: (
|
||||
process_data_embed_file_contents_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_RAW_JSON: (
|
||||
process_data_raw_json_embed_arg,
|
||||
instance.data,
|
||||
),
|
||||
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
|
||||
process_data_embed_raw_json_file_arg,
|
||||
instance.data,
|
||||
),
|
||||
}
|
||||
|
||||
for arg in request_item_args:
|
||||
processor_func, target_dict = rules[arg.sep]
|
||||
value = processor_func(arg)
|
||||
target_dict[arg.key] = value
|
||||
|
||||
if arg.sep in SEPARATORS_GROUP_MULTIPART:
|
||||
instance.multipart_data[arg.key] = value
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
JSONType = Union[str, bool, int, list, dict]
|
||||
|
||||
|
||||
def process_header_arg(arg: KeyValueArg) -> Optional[str]:
|
||||
return arg.value or None
|
||||
|
||||
|
||||
def process_empty_header_arg(arg: KeyValueArg) -> str:
|
||||
if not arg.value:
|
||||
return arg.value
|
||||
raise ParseError(
|
||||
f'Invalid item {arg.orig!r} (to specify an empty header use `Header;`)'
|
||||
)
|
||||
|
||||
|
||||
def process_query_param_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
|
||||
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
|
||||
filename = parts[0]
|
||||
mime_type = parts[1] if len(parts) > 1 else None
|
||||
try:
|
||||
f = open(os.path.expanduser(filename), 'rb')
|
||||
except OSError as e:
|
||||
raise ParseError(f'{arg.orig!r}: {e}')
|
||||
return (
|
||||
os.path.basename(filename),
|
||||
f,
|
||||
mime_type or get_content_type(filename),
|
||||
)
|
||||
|
||||
|
||||
def process_data_item_arg(arg: KeyValueArg) -> str:
|
||||
return arg.value
|
||||
|
||||
|
||||
def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
|
||||
return load_text_file(arg)
|
||||
|
||||
|
||||
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
|
||||
contents = load_text_file(arg)
|
||||
value = load_json(arg, contents)
|
||||
return value
|
||||
|
||||
|
||||
def process_data_raw_json_embed_arg(arg: KeyValueArg) -> JSONType:
|
||||
value = load_json(arg, arg.value)
|
||||
return value
|
||||
|
||||
|
||||
def load_text_file(item: KeyValueArg) -> str:
|
||||
path = item.value
|
||||
try:
|
||||
with open(os.path.expanduser(path), 'rb') as f:
|
||||
return f.read().decode()
|
||||
except OSError as e:
|
||||
raise ParseError(f'{item.orig!r}: {e}')
|
||||
except UnicodeDecodeError:
|
||||
raise ParseError(
|
||||
f'{item.orig!r}: cannot embed the content of {item.value!r},'
|
||||
' not a UTF-8 or ASCII-encoded text file'
|
||||
)
|
||||
|
||||
|
||||
def load_json(arg: KeyValueArg, contents: str) -> JSONType:
|
||||
try:
|
||||
return load_json_preserve_order_and_dupe_keys(contents)
|
||||
except ValueError as e:
|
||||
raise ParseError(f'{arg.orig!r}: {e}')
|
360
httpie/client.py
360
httpie/client.py
@ -1,113 +1,201 @@
|
||||
import argparse
|
||||
import http.client
|
||||
import json
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable, Union
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from httpie import sessions
|
||||
from httpie import __version__
|
||||
from httpie.compat import str
|
||||
from httpie.input import SSL_VERSION_ARG_MAPPING
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.utils import repr_dict_nice
|
||||
|
||||
try:
|
||||
# https://urllib3.readthedocs.io/en/latest/security.html
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
except (ImportError, AttributeError):
|
||||
# In some rare cases, the user may have an old version of the requests
|
||||
# or urllib3, and there is no method called "disable_warnings." In these
|
||||
# cases, we don't need to call the method.
|
||||
# They may get some noisy output but execution shouldn't die. Move on.
|
||||
pass
|
||||
# noinspection PyPackageRequirements
|
||||
import urllib3
|
||||
from . import __version__
|
||||
from .cli.dicts import RequestHeadersDict
|
||||
from .encoding import UTF8
|
||||
from .plugins.registry import plugin_manager
|
||||
from .sessions import get_httpie_session
|
||||
from .ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
|
||||
from .uploads import (
|
||||
compress_request, prepare_request_body,
|
||||
get_multipart_data_and_content_type,
|
||||
)
|
||||
from .utils import get_expired_cookies, repr_dict
|
||||
|
||||
|
||||
FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8'
|
||||
urllib3.disable_warnings()
|
||||
|
||||
FORM_CONTENT_TYPE = f'application/x-www-form-urlencoded; charset={UTF8}'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ACCEPT = '{0}, */*'.format(JSON_CONTENT_TYPE)
|
||||
DEFAULT_UA = 'HTTPie/%s' % __version__
|
||||
JSON_ACCEPT = f'{JSON_CONTENT_TYPE}, */*;q=0.5'
|
||||
DEFAULT_UA = f'HTTPie/{__version__}'
|
||||
|
||||
|
||||
class HTTPieHTTPAdapter(HTTPAdapter):
|
||||
def collect_messages(
|
||||
args: argparse.Namespace,
|
||||
config_dir: Path,
|
||||
request_body_read_callback: Callable[[bytes], None] = None,
|
||||
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
|
||||
httpie_session = None
|
||||
httpie_session_headers = None
|
||||
if args.session or args.session_read_only:
|
||||
httpie_session = get_httpie_session(
|
||||
config_dir=config_dir,
|
||||
session_name=args.session or args.session_read_only,
|
||||
host=args.headers.get('Host'),
|
||||
url=args.url,
|
||||
)
|
||||
httpie_session_headers = httpie_session.headers
|
||||
|
||||
def __init__(self, ssl_version=None, **kwargs):
|
||||
self._ssl_version = ssl_version
|
||||
super(HTTPieHTTPAdapter, self).__init__(**kwargs)
|
||||
|
||||
def init_poolmanager(self, *args, **kwargs):
|
||||
kwargs['ssl_version'] = self._ssl_version
|
||||
super(HTTPieHTTPAdapter, self).init_poolmanager(*args, **kwargs)
|
||||
|
||||
|
||||
def get_requests_session(ssl_version):
|
||||
requests_session = requests.Session()
|
||||
requests_session.mount(
|
||||
'https://',
|
||||
HTTPieHTTPAdapter(ssl_version=ssl_version)
|
||||
request_kwargs = make_request_kwargs(
|
||||
args=args,
|
||||
base_headers=httpie_session_headers,
|
||||
request_body_read_callback=request_body_read_callback
|
||||
)
|
||||
for cls in plugin_manager.get_transport_plugins():
|
||||
transport_plugin = cls()
|
||||
requests_session.mount(prefix=transport_plugin.prefix,
|
||||
adapter=transport_plugin.get_adapter())
|
||||
send_kwargs = make_send_kwargs(args)
|
||||
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
|
||||
requests_session = build_requests_session(
|
||||
ssl_version=args.ssl_version,
|
||||
ciphers=args.ciphers,
|
||||
verify=bool(send_kwargs_mergeable_from_env['verify'])
|
||||
)
|
||||
|
||||
if httpie_session:
|
||||
httpie_session.update_headers(request_kwargs['headers'])
|
||||
requests_session.cookies = httpie_session.cookies
|
||||
if args.auth_plugin:
|
||||
# Save auth from CLI to HTTPie session.
|
||||
httpie_session.auth = {
|
||||
'type': args.auth_plugin.auth_type,
|
||||
'raw_auth': args.auth_plugin.raw_auth,
|
||||
}
|
||||
elif httpie_session.auth:
|
||||
# Apply auth from HTTPie session
|
||||
request_kwargs['auth'] = httpie_session.auth
|
||||
|
||||
if args.debug:
|
||||
# TODO: reflect the split between request and send kwargs.
|
||||
dump_request(request_kwargs)
|
||||
|
||||
request = requests.Request(**request_kwargs)
|
||||
prepared_request = requests_session.prepare_request(request)
|
||||
if args.path_as_is:
|
||||
prepared_request.url = ensure_path_as_is(
|
||||
orig_url=args.url,
|
||||
prepped_url=prepared_request.url,
|
||||
)
|
||||
if args.compress and prepared_request.body:
|
||||
compress_request(
|
||||
request=prepared_request,
|
||||
always=args.compress > 1,
|
||||
)
|
||||
response_count = 0
|
||||
expired_cookies = []
|
||||
while prepared_request:
|
||||
yield prepared_request
|
||||
if not args.offline:
|
||||
send_kwargs_merged = requests_session.merge_environment_settings(
|
||||
url=prepared_request.url,
|
||||
**send_kwargs_mergeable_from_env,
|
||||
)
|
||||
with max_headers(args.max_headers):
|
||||
response = requests_session.send(
|
||||
request=prepared_request,
|
||||
**send_kwargs_merged,
|
||||
**send_kwargs,
|
||||
)
|
||||
|
||||
expired_cookies += get_expired_cookies(
|
||||
response.headers.get('Set-Cookie', '')
|
||||
)
|
||||
|
||||
response_count += 1
|
||||
if response.next:
|
||||
if args.max_redirects and response_count == args.max_redirects:
|
||||
raise requests.TooManyRedirects
|
||||
if args.follow:
|
||||
prepared_request = response.next
|
||||
if args.all:
|
||||
yield response
|
||||
continue
|
||||
yield response
|
||||
break
|
||||
|
||||
if httpie_session:
|
||||
if httpie_session.is_new() or not args.session_read_only:
|
||||
httpie_session.cookies = requests_session.cookies
|
||||
httpie_session.remove_cookies(
|
||||
# TODO: take path & domain into account?
|
||||
cookie['name'] for cookie in expired_cookies
|
||||
)
|
||||
httpie_session.save()
|
||||
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
@contextmanager
|
||||
def max_headers(limit):
|
||||
# <https://github.com/httpie/httpie/issues/802>
|
||||
# noinspection PyUnresolvedReferences
|
||||
orig = http.client._MAXHEADERS
|
||||
http.client._MAXHEADERS = limit or float('Inf')
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
http.client._MAXHEADERS = orig
|
||||
|
||||
|
||||
def build_requests_session(
|
||||
verify: bool,
|
||||
ssl_version: str = None,
|
||||
ciphers: str = None,
|
||||
) -> requests.Session:
|
||||
requests_session = requests.Session()
|
||||
|
||||
# Install our adapter.
|
||||
https_adapter = HTTPieHTTPSAdapter(
|
||||
ciphers=ciphers,
|
||||
verify=verify,
|
||||
ssl_version=(
|
||||
AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
|
||||
if ssl_version else None
|
||||
),
|
||||
)
|
||||
requests_session.mount('https://', https_adapter)
|
||||
|
||||
# Install adapters from plugins.
|
||||
for plugin_cls in plugin_manager.get_transport_plugins():
|
||||
transport_plugin = plugin_cls()
|
||||
requests_session.mount(
|
||||
prefix=transport_plugin.prefix,
|
||||
adapter=transport_plugin.get_adapter(),
|
||||
)
|
||||
|
||||
return requests_session
|
||||
|
||||
|
||||
def get_response(args, config_dir):
|
||||
"""Send the request and return a `request.Response`."""
|
||||
|
||||
ssl_version = None
|
||||
if args.ssl_version:
|
||||
ssl_version = SSL_VERSION_ARG_MAPPING[args.ssl_version]
|
||||
|
||||
requests_session = get_requests_session(ssl_version)
|
||||
requests_session.max_redirects = args.max_redirects
|
||||
|
||||
if not args.session and not args.session_read_only:
|
||||
kwargs = get_requests_kwargs(args)
|
||||
if args.debug:
|
||||
dump_request(kwargs)
|
||||
response = requests_session.request(**kwargs)
|
||||
else:
|
||||
response = sessions.get_response(
|
||||
requests_session=requests_session,
|
||||
args=args,
|
||||
config_dir=config_dir,
|
||||
session_name=args.session or args.session_read_only,
|
||||
read_only=bool(args.session_read_only),
|
||||
)
|
||||
|
||||
return response
|
||||
def dump_request(kwargs: dict):
|
||||
sys.stderr.write(
|
||||
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
|
||||
|
||||
|
||||
def dump_request(kwargs):
|
||||
sys.stderr.write('\n>>> requests.request(**%s)\n\n'
|
||||
% repr_dict_nice(kwargs))
|
||||
|
||||
|
||||
def finalize_headers(headers):
|
||||
final_headers = {}
|
||||
def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
|
||||
final_headers = RequestHeadersDict()
|
||||
for name, value in headers.items():
|
||||
if value is not None:
|
||||
|
||||
# >leading or trailing LWS MAY be removed without
|
||||
# >changing the semantics of the field value"
|
||||
# -https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html
|
||||
# “leading or trailing LWS MAY be removed without
|
||||
# changing the semantics of the field value”
|
||||
# <https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html>
|
||||
# Also, requests raises `InvalidHeader` for leading spaces.
|
||||
value = value.strip()
|
||||
|
||||
if isinstance(value, str):
|
||||
# See: https://github.com/jakubroztocil/httpie/issues/212
|
||||
value = value.encode('utf8')
|
||||
|
||||
# See <https://github.com/httpie/httpie/issues/212>
|
||||
value = value.encode()
|
||||
final_headers[name] = value
|
||||
return final_headers
|
||||
|
||||
|
||||
def get_default_headers(args):
|
||||
default_headers = CaseInsensitiveDict({
|
||||
def make_default_headers(args: argparse.Namespace) -> RequestHeadersDict:
|
||||
default_headers = RequestHeadersDict({
|
||||
'User-Agent': DEFAULT_UA
|
||||
})
|
||||
|
||||
@ -124,11 +212,42 @@ def get_default_headers(args):
|
||||
return default_headers
|
||||
|
||||
|
||||
def get_requests_kwargs(args, base_headers=None):
|
||||
def make_send_kwargs(args: argparse.Namespace) -> dict:
|
||||
return {
|
||||
'timeout': args.timeout or None,
|
||||
'allow_redirects': False,
|
||||
}
|
||||
|
||||
|
||||
def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
|
||||
cert = None
|
||||
if args.cert:
|
||||
cert = args.cert
|
||||
if args.cert_key:
|
||||
cert = cert, args.cert_key
|
||||
return {
|
||||
'proxies': {p.key: p.value for p in args.proxy},
|
||||
'stream': True,
|
||||
'verify': {
|
||||
'yes': True,
|
||||
'true': True,
|
||||
'no': False,
|
||||
'false': False,
|
||||
}.get(args.verify.lower(), args.verify),
|
||||
'cert': cert,
|
||||
}
|
||||
|
||||
|
||||
def make_request_kwargs(
|
||||
args: argparse.Namespace,
|
||||
base_headers: RequestHeadersDict = None,
|
||||
request_body_read_callback=lambda chunk: chunk
|
||||
) -> dict:
|
||||
"""
|
||||
Translate our `args` into `requests.request` keyword arguments.
|
||||
Translate our `args` into `requests.Request` keyword arguments.
|
||||
|
||||
"""
|
||||
files = args.files
|
||||
# Serialize JSON data, if needed.
|
||||
data = args.data
|
||||
auto_json = data and not args.form
|
||||
@ -141,37 +260,60 @@ def get_requests_kwargs(args, base_headers=None):
|
||||
data = ''
|
||||
|
||||
# Finalize headers.
|
||||
headers = get_default_headers(args)
|
||||
headers = make_default_headers(args)
|
||||
if base_headers:
|
||||
headers.update(base_headers)
|
||||
headers.update(args.headers)
|
||||
if args.offline and args.chunked and 'Transfer-Encoding' not in headers:
|
||||
# When online, we let requests set the header instead to be able more
|
||||
# easily verify chunking is taking place.
|
||||
headers['Transfer-Encoding'] = 'chunked'
|
||||
headers = finalize_headers(headers)
|
||||
|
||||
cert = None
|
||||
if args.cert:
|
||||
cert = args.cert
|
||||
if args.cert_key:
|
||||
cert = cert, args.cert_key
|
||||
if (args.form and files) or args.multipart:
|
||||
data, headers['Content-Type'] = get_multipart_data_and_content_type(
|
||||
data=args.multipart_data,
|
||||
boundary=args.boundary,
|
||||
content_type=args.headers.get('Content-Type'),
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
'stream': True,
|
||||
return {
|
||||
'method': args.method.lower(),
|
||||
'url': args.url,
|
||||
'headers': headers,
|
||||
'data': data,
|
||||
'verify': {
|
||||
'yes': True,
|
||||
'true': True,
|
||||
'no': False,
|
||||
'false': False,
|
||||
}.get(args.verify.lower(), args.verify),
|
||||
'cert': cert,
|
||||
'timeout': args.timeout,
|
||||
'data': prepare_request_body(
|
||||
body=data,
|
||||
body_read_callback=request_body_read_callback,
|
||||
chunked=args.chunked,
|
||||
offline=args.offline,
|
||||
content_length_header_value=headers.get('Content-Length'),
|
||||
),
|
||||
'auth': args.auth,
|
||||
'proxies': {p.key: p.value for p in args.proxy},
|
||||
'files': args.files,
|
||||
'allow_redirects': args.follow,
|
||||
'params': args.params,
|
||||
'params': args.params.items(),
|
||||
}
|
||||
|
||||
return kwargs
|
||||
|
||||
def ensure_path_as_is(orig_url: str, prepped_url: str) -> str:
|
||||
"""
|
||||
Handle `--path-as-is` by replacing the path component of the prepared
|
||||
URL with the path component from the original URL. Other parts stay
|
||||
untouched because other (welcome) processing on the URL might have
|
||||
taken place.
|
||||
|
||||
<https://github.com/httpie/httpie/issues/895>
|
||||
|
||||
|
||||
<https://ec.haxx.se/http/http-basics#path-as-is>
|
||||
<https://curl.haxx.se/libcurl/c/CURLOPT_PATH_AS_IS.html>
|
||||
|
||||
>>> ensure_path_as_is('http://foo/../', 'http://foo/?foo=bar')
|
||||
'http://foo/../?foo=bar'
|
||||
|
||||
"""
|
||||
parsed_orig, parsed_prepped = urlparse(orig_url), urlparse(prepped_url)
|
||||
final_dict = {
|
||||
# noinspection PyProtectedMember
|
||||
**parsed_prepped._asdict(),
|
||||
'path': parsed_orig.path,
|
||||
}
|
||||
return urlunparse(tuple(final_dict.values()))
|
||||
|
@ -1,39 +1,54 @@
|
||||
"""
|
||||
Python 2.7, and 3.x compatibility.
|
||||
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
||||
is_py2 = sys.version_info[0] == 2
|
||||
is_py27 = sys.version_info[:2] == (2, 7)
|
||||
is_py3 = sys.version_info[0] == 3
|
||||
is_pypy = 'pypy' in sys.version.lower()
|
||||
is_windows = 'win32' in str(sys.platform).lower()
|
||||
|
||||
|
||||
if is_py2:
|
||||
# noinspection PyShadowingBuiltins
|
||||
bytes = str
|
||||
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
|
||||
str = unicode
|
||||
elif is_py3:
|
||||
# noinspection PyShadowingBuiltins
|
||||
str = str
|
||||
# noinspection PyShadowingBuiltins
|
||||
bytes = bytes
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
# Can be removed once we drop Python <3.8 support.
|
||||
# Taken from `django.utils.functional.cached_property`.
|
||||
class cached_property:
|
||||
"""
|
||||
Decorator that converts a method with a single self argument into a
|
||||
property cached on the instance.
|
||||
|
||||
A cached property can be made out of an existing method:
|
||||
(e.g. ``url = cached_property(get_absolute_url)``).
|
||||
The optional ``name`` argument is obsolete as of Python 3.6 and will be
|
||||
deprecated in Django 4.0 (#30127).
|
||||
"""
|
||||
name = None
|
||||
|
||||
try: # pragma: no cover
|
||||
# noinspection PyUnresolvedReferences,PyCompatibility
|
||||
from urllib.parse import urlsplit
|
||||
except ImportError: # pragma: no cover
|
||||
# noinspection PyUnresolvedReferences,PyCompatibility
|
||||
from urlparse import urlsplit
|
||||
@staticmethod
|
||||
def func(instance):
|
||||
raise TypeError(
|
||||
'Cannot use cached_property instance without calling '
|
||||
'__set_name__() on it.'
|
||||
)
|
||||
|
||||
try: # pragma: no cover
|
||||
# noinspection PyCompatibility
|
||||
from urllib.request import urlopen
|
||||
except ImportError: # pragma: no cover
|
||||
# noinspection PyCompatibility,PyUnresolvedReferences
|
||||
from urllib2 import urlopen
|
||||
def __init__(self, func, name=None):
|
||||
self.real_func = func
|
||||
self.__doc__ = getattr(func, '__doc__')
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if self.name is None:
|
||||
self.name = name
|
||||
self.func = self.real_func
|
||||
elif name != self.name:
|
||||
raise TypeError(
|
||||
"Cannot assign the same cached_property to two different names "
|
||||
"(%r and %r)." % (self.name, name)
|
||||
)
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Call the function and put the return value in instance.__dict__ so that
|
||||
subsequent attribute access on the instance returns the cached value
|
||||
instead of calling cached_property.__get__().
|
||||
"""
|
||||
if instance is None:
|
||||
return self
|
||||
res = instance.__dict__[self.name] = self.func(instance)
|
||||
return res
|
||||
|
158
httpie/config.py
158
httpie/config.py
@ -1,59 +1,97 @@
|
||||
import os
|
||||
import json
|
||||
import errno
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from httpie import __version__
|
||||
from httpie.compat import is_windows
|
||||
from . import __version__
|
||||
from .compat import is_windows
|
||||
from .encoding import UTF8
|
||||
|
||||
|
||||
DEFAULT_CONFIG_DIR = str(os.environ.get(
|
||||
'HTTPIE_CONFIG_DIR',
|
||||
os.path.expanduser('~/.httpie') if not is_windows else
|
||||
os.path.expandvars(r'%APPDATA%\\httpie')
|
||||
))
|
||||
ENV_XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
|
||||
ENV_HTTPIE_CONFIG_DIR = 'HTTPIE_CONFIG_DIR'
|
||||
DEFAULT_CONFIG_DIRNAME = 'httpie'
|
||||
DEFAULT_RELATIVE_XDG_CONFIG_HOME = Path('.config')
|
||||
DEFAULT_RELATIVE_LEGACY_CONFIG_DIR = Path('.httpie')
|
||||
DEFAULT_WINDOWS_CONFIG_DIR = Path(
|
||||
os.path.expandvars('%APPDATA%')) / DEFAULT_CONFIG_DIRNAME
|
||||
|
||||
|
||||
def get_default_config_dir() -> Path:
|
||||
"""
|
||||
Return the path to the httpie configuration directory.
|
||||
|
||||
This directory isn't guaranteed to exist, and nor are any of its
|
||||
ancestors (only the legacy ~/.httpie, if returned, is guaranteed to exist).
|
||||
|
||||
XDG Base Directory Specification support:
|
||||
|
||||
<https://wiki.archlinux.org/index.php/XDG_Base_Directory>
|
||||
|
||||
$XDG_CONFIG_HOME is supported; $XDG_CONFIG_DIRS is not
|
||||
|
||||
"""
|
||||
# 1. explicitly set through env
|
||||
env_config_dir = os.environ.get(ENV_HTTPIE_CONFIG_DIR)
|
||||
if env_config_dir:
|
||||
return Path(env_config_dir)
|
||||
|
||||
# 2. Windows
|
||||
if is_windows:
|
||||
return DEFAULT_WINDOWS_CONFIG_DIR
|
||||
|
||||
home_dir = Path.home()
|
||||
|
||||
# 3. legacy ~/.httpie
|
||||
legacy_config_dir = home_dir / DEFAULT_RELATIVE_LEGACY_CONFIG_DIR
|
||||
if legacy_config_dir.exists():
|
||||
return legacy_config_dir
|
||||
|
||||
# 4. XDG
|
||||
xdg_config_home_dir = os.environ.get(
|
||||
ENV_XDG_CONFIG_HOME, # 4.1. explicit
|
||||
home_dir / DEFAULT_RELATIVE_XDG_CONFIG_HOME # 4.2. default
|
||||
)
|
||||
return Path(xdg_config_home_dir) / DEFAULT_CONFIG_DIRNAME
|
||||
|
||||
|
||||
DEFAULT_CONFIG_DIR = get_default_config_dir()
|
||||
|
||||
|
||||
class ConfigFileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseConfigDict(dict):
|
||||
|
||||
name = None
|
||||
helpurl = None
|
||||
about = None
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
def __init__(self, path: Path):
|
||||
super().__init__()
|
||||
self.path = path
|
||||
|
||||
def _get_path(self):
|
||||
"""Return the config file path without side-effects."""
|
||||
raise NotImplementedError()
|
||||
def ensure_directory(self):
|
||||
self.path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Return the config file path creating basedir, if needed."""
|
||||
path = self._get_path()
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path), mode=0o700)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return path
|
||||
|
||||
def is_new(self):
|
||||
return not os.path.exists(self._get_path())
|
||||
def is_new(self) -> bool:
|
||||
return not self.path.exists()
|
||||
|
||||
def load(self):
|
||||
config_type = type(self).__name__.lower()
|
||||
try:
|
||||
with open(self.path, 'rt') as f:
|
||||
with self.path.open(encoding=UTF8) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
'Invalid %s JSON: %s [%s]' %
|
||||
(type(self).__name__, str(e), self.path)
|
||||
raise ConfigFileError(
|
||||
f'invalid {config_type} file: {e} [{self.path}]'
|
||||
)
|
||||
self.update(data)
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise ConfigFileError(f'cannot read {config_type} file: {e}')
|
||||
|
||||
def save(self):
|
||||
self['__meta__'] = {
|
||||
@ -65,48 +103,28 @@ class BaseConfigDict(dict):
|
||||
if self.about:
|
||||
self['__meta__']['about'] = self.about
|
||||
|
||||
with open(self.path, 'w') as f:
|
||||
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
|
||||
f.write('\n')
|
||||
self.ensure_directory()
|
||||
|
||||
def delete(self):
|
||||
try:
|
||||
os.unlink(self.path)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
json_string = json.dumps(
|
||||
obj=self,
|
||||
indent=4,
|
||||
sort_keys=True,
|
||||
ensure_ascii=True,
|
||||
)
|
||||
self.path.write_text(json_string + '\n', encoding=UTF8)
|
||||
|
||||
|
||||
class Config(BaseConfigDict):
|
||||
|
||||
name = 'config'
|
||||
helpurl = 'https://httpie.org/doc#config'
|
||||
about = 'HTTPie configuration file'
|
||||
|
||||
FILENAME = 'config.json'
|
||||
DEFAULTS = {
|
||||
'default_options': []
|
||||
}
|
||||
|
||||
def __init__(self, directory=DEFAULT_CONFIG_DIR):
|
||||
super(Config, self).__init__()
|
||||
def __init__(self, directory: Union[str, Path] = DEFAULT_CONFIG_DIR):
|
||||
self.directory = Path(directory)
|
||||
super().__init__(path=self.directory / self.FILENAME)
|
||||
self.update(self.DEFAULTS)
|
||||
self.directory = directory
|
||||
|
||||
def load(self):
|
||||
super(Config, self).load()
|
||||
self._migrate_implicit_content_type()
|
||||
|
||||
def _get_path(self):
|
||||
return os.path.join(self.directory, self.name + '.json')
|
||||
|
||||
def _migrate_implicit_content_type(self):
|
||||
"""Migrate the removed implicit_content_type config option"""
|
||||
try:
|
||||
implicit_content_type = self.pop('implicit_content_type')
|
||||
except KeyError:
|
||||
self.save()
|
||||
else:
|
||||
if implicit_content_type == 'form':
|
||||
self['default_options'].insert(0, '--form')
|
||||
self.save()
|
||||
self.load()
|
||||
@property
|
||||
def default_options(self) -> list:
|
||||
return self['default_options']
|
||||
|
@ -1,16 +1,22 @@
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import IO, Optional
|
||||
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
curses = None # Compiled w/o curses
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.config import DEFAULT_CONFIG_DIR, Config
|
||||
from .compat import is_windows
|
||||
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
||||
from .encoding import UTF8
|
||||
|
||||
from httpie.utils import repr_dict_nice
|
||||
from .utils import repr_dict
|
||||
|
||||
|
||||
class Environment(object):
|
||||
class Environment:
|
||||
"""
|
||||
Information about the execution context
|
||||
(standard streams, config directory, etc).
|
||||
@ -20,17 +26,18 @@ class Environment(object):
|
||||
is used by the test suite to simulate various scenarios.
|
||||
|
||||
"""
|
||||
is_windows = is_windows
|
||||
config_dir = DEFAULT_CONFIG_DIR
|
||||
stdin = sys.stdin
|
||||
stdin_isatty = stdin.isatty()
|
||||
stdin_encoding = None
|
||||
stdout = sys.stdout
|
||||
stdout_isatty = stdout.isatty()
|
||||
stdout_encoding = None
|
||||
stderr = sys.stderr
|
||||
stderr_isatty = stderr.isatty()
|
||||
is_windows: bool = is_windows
|
||||
config_dir: Path = DEFAULT_CONFIG_DIR
|
||||
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
||||
stdin_isatty: bool = stdin.isatty() if stdin else False
|
||||
stdin_encoding: str = None
|
||||
stdout: IO = sys.stdout
|
||||
stdout_isatty: bool = stdout.isatty()
|
||||
stdout_encoding: str = None
|
||||
stderr: IO = sys.stderr
|
||||
stderr_isatty: bool = stderr.isatty()
|
||||
colors = 256
|
||||
program_name: str = 'http'
|
||||
if not is_windows:
|
||||
if curses:
|
||||
try:
|
||||
@ -51,7 +58,7 @@ class Environment(object):
|
||||
)
|
||||
del colorama
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, devnull=None, **kwargs):
|
||||
"""
|
||||
Use keyword arguments to overwrite
|
||||
any of the class attributes for this instance.
|
||||
@ -60,40 +67,59 @@ class Environment(object):
|
||||
assert all(hasattr(type(self), attr) for attr in kwargs.keys())
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
# Keyword arguments > stream.encoding > default utf8
|
||||
if self.stdin_encoding is None:
|
||||
# The original STDERR unaffected by --quiet’ing.
|
||||
self._orig_stderr = self.stderr
|
||||
self._devnull = devnull
|
||||
|
||||
# Keyword arguments > stream.encoding > default UTF-8
|
||||
if self.stdin and self.stdin_encoding is None:
|
||||
self.stdin_encoding = getattr(
|
||||
self.stdin, 'encoding', None) or 'utf8'
|
||||
self.stdin, 'encoding', None) or UTF8
|
||||
if self.stdout_encoding is None:
|
||||
actual_stdout = self.stdout
|
||||
if is_windows:
|
||||
# noinspection PyUnresolvedReferences
|
||||
from colorama import AnsiToWin32
|
||||
if isinstance(self.stdout, AnsiToWin32):
|
||||
# noinspection PyUnresolvedReferences
|
||||
actual_stdout = self.stdout.wrapped
|
||||
self.stdout_encoding = getattr(
|
||||
actual_stdout, 'encoding', None) or 'utf8'
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
if not hasattr(self, '_config'):
|
||||
self._config = Config(directory=self.config_dir)
|
||||
if self._config.is_new():
|
||||
self._config.save()
|
||||
else:
|
||||
self._config.load()
|
||||
return self._config
|
||||
actual_stdout, 'encoding', None) or UTF8
|
||||
|
||||
def __str__(self):
|
||||
defaults = dict(type(self).__dict__)
|
||||
actual = dict(defaults)
|
||||
actual.update(self.__dict__)
|
||||
actual['config'] = self.config
|
||||
return repr_dict_nice(dict(
|
||||
(key, value)
|
||||
return repr_dict({
|
||||
key: value
|
||||
for key, value in actual.items()
|
||||
if not key.startswith('_'))
|
||||
)
|
||||
if not key.startswith('_')
|
||||
})
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} {1}>'.format(type(self).__name__, str(self))
|
||||
return f'<{type(self).__name__} {self}>'
|
||||
|
||||
_config: Config = None
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
config = self._config
|
||||
if not config:
|
||||
self._config = config = Config(directory=self.config_dir)
|
||||
if not config.is_new():
|
||||
try:
|
||||
config.load()
|
||||
except ConfigFileError as e:
|
||||
self.log_error(e, level='warning')
|
||||
return config
|
||||
|
||||
@property
|
||||
def devnull(self) -> IO:
|
||||
if self._devnull is None:
|
||||
self._devnull = open(os.devnull, 'w+')
|
||||
return self._devnull
|
||||
|
||||
def log_error(self, msg, level='error'):
|
||||
assert level in ['error', 'warning']
|
||||
self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')
|
||||
|
360
httpie/core.py
360
httpie/core.py
@ -1,175 +1,25 @@
|
||||
"""This module provides the main functionality of HTTPie.
|
||||
|
||||
Invocation flow:
|
||||
|
||||
1. Read, validate and process the input (args, `stdin`).
|
||||
2. Create and send a request.
|
||||
3. Stream, and possibly process and format, the parts
|
||||
of the request-response exchange selected by output options.
|
||||
4. Simultaneously write to `stdout`
|
||||
5. Exit.
|
||||
|
||||
"""
|
||||
import sys
|
||||
import errno
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
import requests
|
||||
from requests import __version__ as requests_version
|
||||
from pygments import __version__ as pygments_version
|
||||
from requests import __version__ as requests_version
|
||||
|
||||
from httpie import __version__ as httpie_version, ExitStatus
|
||||
from httpie.compat import str, bytes, is_py3
|
||||
from httpie.client import get_response
|
||||
from httpie.downloads import Downloader
|
||||
from httpie.context import Environment
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.output.streams import (
|
||||
build_output_stream,
|
||||
write_stream,
|
||||
write_stream_with_colors_win_py3
|
||||
)
|
||||
from . import __version__ as httpie_version
|
||||
from .cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
|
||||
from .client import collect_messages
|
||||
from .context import Environment
|
||||
from .downloads import Downloader
|
||||
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
|
||||
from .plugins.registry import plugin_manager
|
||||
from .status import ExitStatus, http_status_to_exit_status
|
||||
|
||||
|
||||
def get_exit_status(http_status, follow=False):
|
||||
"""Translate HTTP status code to exit status code."""
|
||||
if 300 <= http_status <= 399 and not follow:
|
||||
# Redirect
|
||||
return ExitStatus.ERROR_HTTP_3XX
|
||||
elif 400 <= http_status <= 499:
|
||||
# Client Error
|
||||
return ExitStatus.ERROR_HTTP_4XX
|
||||
elif 500 <= http_status <= 599:
|
||||
# Server Error
|
||||
return ExitStatus.ERROR_HTTP_5XX
|
||||
else:
|
||||
return ExitStatus.SUCCESS
|
||||
|
||||
|
||||
def print_debug_info(env):
|
||||
env.stderr.writelines([
|
||||
'HTTPie %s\n' % httpie_version,
|
||||
'Requests %s\n' % requests_version,
|
||||
'Pygments %s\n' % pygments_version,
|
||||
'Python %s\n%s\n' % (sys.version, sys.executable),
|
||||
'%s %s' % (platform.system(), platform.release()),
|
||||
])
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(env))
|
||||
env.stderr.write('\n')
|
||||
|
||||
|
||||
def decode_args(args, stdin_encoding):
|
||||
"""
|
||||
Convert all bytes args to str
|
||||
by decoding them using stdin encoding.
|
||||
|
||||
"""
|
||||
return [
|
||||
arg.decode(stdin_encoding)
|
||||
if type(arg) == bytes else arg
|
||||
for arg in args
|
||||
]
|
||||
|
||||
|
||||
def program(args, env, log_error):
|
||||
"""
|
||||
The main program without error handling
|
||||
|
||||
:param args: parsed args (argparse.Namespace)
|
||||
:type env: Environment
|
||||
:param log_error: error log function
|
||||
:return: status code
|
||||
|
||||
"""
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
downloader = None
|
||||
show_traceback = args.debug or args.traceback
|
||||
|
||||
try:
|
||||
if args.download:
|
||||
args.follow = True # --download implies --follow.
|
||||
downloader = Downloader(
|
||||
output_file=args.output_file,
|
||||
progress_file=env.stderr,
|
||||
resume=args.download_resume
|
||||
)
|
||||
downloader.pre_request(args.headers)
|
||||
|
||||
final_response = get_response(args, config_dir=env.config.directory)
|
||||
if args.all:
|
||||
responses = final_response.history + [final_response]
|
||||
else:
|
||||
responses = [final_response]
|
||||
|
||||
for response in responses:
|
||||
|
||||
if args.check_status or downloader:
|
||||
exit_status = get_exit_status(
|
||||
http_status=response.status_code,
|
||||
follow=args.follow
|
||||
)
|
||||
if not env.stdout_isatty and exit_status != ExitStatus.SUCCESS:
|
||||
log_error(
|
||||
'HTTP %s %s', response.raw.status, response.raw.reason,
|
||||
level='warning'
|
||||
)
|
||||
|
||||
write_stream_kwargs = {
|
||||
'stream': build_output_stream(
|
||||
args=args,
|
||||
env=env,
|
||||
request=response.request,
|
||||
response=response,
|
||||
output_options=(
|
||||
args.output_options
|
||||
if response is final_response
|
||||
else args.output_options_history
|
||||
)
|
||||
),
|
||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||
'outfile': env.stdout,
|
||||
'flush': env.stdout_isatty or args.stream
|
||||
}
|
||||
try:
|
||||
if env.is_windows and is_py3 and 'colors' in args.prettify:
|
||||
write_stream_with_colors_win_py3(**write_stream_kwargs)
|
||||
else:
|
||||
write_stream(**write_stream_kwargs)
|
||||
except IOError as e:
|
||||
if not show_traceback and e.errno == errno.EPIPE:
|
||||
# Ignore broken pipes unless --traceback.
|
||||
env.stderr.write('\n')
|
||||
else:
|
||||
raise
|
||||
|
||||
if downloader and exit_status == ExitStatus.SUCCESS:
|
||||
# Last response body download.
|
||||
download_stream, download_to = downloader.start(final_response)
|
||||
write_stream(
|
||||
stream=download_stream,
|
||||
outfile=download_to,
|
||||
flush=False,
|
||||
)
|
||||
downloader.finish()
|
||||
if downloader.interrupted:
|
||||
exit_status = ExitStatus.ERROR
|
||||
log_error('Incomplete download: size=%d; downloaded=%d' % (
|
||||
downloader.status.total_size,
|
||||
downloader.status.downloaded
|
||||
))
|
||||
return exit_status
|
||||
|
||||
finally:
|
||||
if downloader and not downloader.finished:
|
||||
downloader.failed()
|
||||
|
||||
if (not isinstance(args, list) and args.output_file
|
||||
and args.output_file_specified):
|
||||
args.output_file.close()
|
||||
|
||||
|
||||
def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
# noinspection PyDefaultArgument
|
||||
def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitStatus:
|
||||
"""
|
||||
The main function.
|
||||
|
||||
@ -179,23 +29,20 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
Return exit status code.
|
||||
|
||||
"""
|
||||
args = decode_args(args, env.stdin_encoding)
|
||||
if '--prompt' in args:
|
||||
from .prompt.cli import cli
|
||||
return cli(sys.argv[2:])
|
||||
|
||||
program_name, *args = args
|
||||
env.program_name = os.path.basename(program_name)
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
plugin_manager.load_installed_plugins()
|
||||
|
||||
def log_error(msg, *args, **kwargs):
|
||||
msg = msg % args
|
||||
level = kwargs.get('level', 'error')
|
||||
assert level in ['error', 'warning']
|
||||
env.stderr.write('\nhttp: %s: %s\n' % (level, msg))
|
||||
|
||||
from httpie.cli import parser
|
||||
from .cli.definition import parser
|
||||
|
||||
if env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
|
||||
if custom_log_error:
|
||||
log_error = custom_log_error
|
||||
|
||||
include_debug_info = '--debug' in args
|
||||
include_traceback = include_debug_info or '--traceback' in args
|
||||
|
||||
@ -207,7 +54,10 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
|
||||
try:
|
||||
parsed_args = parser.parse_args(args=args, env=env)
|
||||
parsed_args = parser.parse_args(
|
||||
args=args,
|
||||
env=env,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
env.stderr.write('\n')
|
||||
if include_traceback:
|
||||
@ -224,7 +74,6 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
exit_status = program(
|
||||
args=parsed_args,
|
||||
env=env,
|
||||
log_error=log_error,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
env.stderr.write('\n')
|
||||
@ -239,22 +88,165 @@ def main(args=sys.argv[1:], env=Environment(), custom_log_error=None):
|
||||
exit_status = ExitStatus.ERROR
|
||||
except requests.Timeout:
|
||||
exit_status = ExitStatus.ERROR_TIMEOUT
|
||||
log_error('Request timed out (%ss).', parsed_args.timeout)
|
||||
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
|
||||
except requests.TooManyRedirects:
|
||||
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
||||
log_error('Too many redirects (--max-redirects=%s).',
|
||||
parsed_args.max_redirects)
|
||||
env.log_error(
|
||||
f'Too many redirects'
|
||||
f' (--max-redirects={parsed_args.max_redirects}).'
|
||||
)
|
||||
except Exception as e:
|
||||
# TODO: Further distinction between expected and unexpected errors.
|
||||
msg = str(e)
|
||||
if hasattr(e, 'request'):
|
||||
request = e.request
|
||||
if hasattr(request, 'url'):
|
||||
msg += ' while doing %s request to URL: %s' % (
|
||||
request.method, request.url)
|
||||
log_error('%s: %s', type(e).__name__, msg)
|
||||
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
|
||||
|
||||
return exit_status
|
||||
|
||||
|
||||
def get_output_options(
|
||||
args: argparse.Namespace,
|
||||
message: Union[requests.PreparedRequest, requests.Response]
|
||||
) -> Tuple[bool, bool]:
|
||||
return {
|
||||
requests.PreparedRequest: (
|
||||
OUT_REQ_HEAD in args.output_options,
|
||||
OUT_REQ_BODY in args.output_options,
|
||||
),
|
||||
requests.Response: (
|
||||
OUT_RESP_HEAD in args.output_options,
|
||||
OUT_RESP_BODY in args.output_options,
|
||||
),
|
||||
}[type(message)]
|
||||
|
||||
|
||||
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
||||
"""
|
||||
The main program without error handling.
|
||||
|
||||
"""
|
||||
# TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere.
|
||||
exit_status = ExitStatus.SUCCESS
|
||||
downloader = None
|
||||
initial_request: Optional[requests.PreparedRequest] = None
|
||||
final_response: Optional[requests.Response] = None
|
||||
|
||||
def separate():
|
||||
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES)
|
||||
|
||||
def request_body_read_callback(chunk: bytes):
|
||||
should_pipe_to_stdout = bool(
|
||||
# Request body output desired
|
||||
OUT_REQ_BODY in args.output_options
|
||||
# & not `.read()` already pre-request (e.g., for compression)
|
||||
and initial_request
|
||||
# & non-EOF chunk
|
||||
and chunk
|
||||
)
|
||||
if should_pipe_to_stdout:
|
||||
msg = requests.PreparedRequest()
|
||||
msg.is_body_upload_chunk = True
|
||||
msg.body = chunk
|
||||
msg.headers = initial_request.headers
|
||||
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
|
||||
|
||||
try:
|
||||
if args.download:
|
||||
args.follow = True # --download implies --follow.
|
||||
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
||||
downloader.pre_request(args.headers)
|
||||
messages = collect_messages(args=args, config_dir=env.config.directory,
|
||||
request_body_read_callback=request_body_read_callback)
|
||||
force_separator = False
|
||||
prev_with_body = False
|
||||
|
||||
# Process messages as they’re generated
|
||||
for message in messages:
|
||||
is_request = isinstance(message, requests.PreparedRequest)
|
||||
with_headers, with_body = get_output_options(args=args, message=message)
|
||||
do_write_body = with_body
|
||||
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
|
||||
# Separate after a previous message with body, if needed. See test_tokens.py.
|
||||
separate()
|
||||
force_separator = False
|
||||
if is_request:
|
||||
if not initial_request:
|
||||
initial_request = message
|
||||
if with_body:
|
||||
is_streamed_upload = not isinstance(message.body, (str, bytes))
|
||||
do_write_body = not is_streamed_upload
|
||||
force_separator = is_streamed_upload and env.stdout_isatty
|
||||
else:
|
||||
final_response = message
|
||||
if args.check_status or downloader:
|
||||
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
|
||||
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet):
|
||||
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
|
||||
write_message(requests_message=message, env=env, args=args, with_headers=with_headers,
|
||||
with_body=do_write_body)
|
||||
prev_with_body = with_body
|
||||
|
||||
# Cleanup
|
||||
if force_separator:
|
||||
separate()
|
||||
if downloader and exit_status == ExitStatus.SUCCESS:
|
||||
# Last response body download.
|
||||
download_stream, download_to = downloader.start(
|
||||
initial_url=initial_request.url,
|
||||
final_response=final_response,
|
||||
)
|
||||
write_stream(stream=download_stream, outfile=download_to, flush=False)
|
||||
downloader.finish()
|
||||
if downloader.interrupted:
|
||||
exit_status = ExitStatus.ERROR
|
||||
env.log_error(
|
||||
f'Incomplete download: size={downloader.status.total_size};'
|
||||
f' downloaded={downloader.status.downloaded}'
|
||||
)
|
||||
return exit_status
|
||||
|
||||
finally:
|
||||
if downloader and not downloader.finished:
|
||||
downloader.failed()
|
||||
if args.output_file and args.output_file_specified:
|
||||
args.output_file.close()
|
||||
|
||||
|
||||
def print_debug_info(env: Environment):
|
||||
env.stderr.writelines([
|
||||
f'HTTPie {httpie_version}\n',
|
||||
f'Requests {requests_version}\n',
|
||||
f'Pygments {pygments_version}\n',
|
||||
f'Python {sys.version}\n{sys.executable}\n',
|
||||
f'{platform.system()} {platform.release()}',
|
||||
])
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(env))
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(plugin_manager))
|
||||
env.stderr.write('\n')
|
||||
|
||||
|
||||
def decode_raw_args(
|
||||
args: List[Union[str, bytes]],
|
||||
stdin_encoding: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
Convert all bytes args to str
|
||||
by decoding them using stdin encoding.
|
||||
|
||||
"""
|
||||
return [
|
||||
arg.decode(stdin_encoding)
|
||||
if type(arg) is bytes else arg
|
||||
for arg in args
|
||||
]
|
||||
|
@ -1,27 +1,26 @@
|
||||
# coding=utf-8
|
||||
"""
|
||||
Download mode implementation.
|
||||
|
||||
"""
|
||||
from __future__ import division
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import errno
|
||||
import mimetypes
|
||||
import threading
|
||||
from time import sleep, time
|
||||
from mailbox import Message
|
||||
from time import sleep, monotonic
|
||||
from typing import IO, Optional, Tuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from httpie.output.streams import RawStream
|
||||
from httpie.models import HTTPResponse
|
||||
from httpie.utils import humanize_bytes
|
||||
from httpie.compat import urlsplit
|
||||
import requests
|
||||
|
||||
from .models import HTTPResponse
|
||||
from .output.streams import RawStream
|
||||
from .utils import humanize_bytes
|
||||
|
||||
|
||||
PARTIAL_CONTENT = 206
|
||||
|
||||
|
||||
CLEAR_LINE = '\r\033[K'
|
||||
PROGRESS = (
|
||||
'{percentage: 6.2f} %'
|
||||
@ -38,11 +37,11 @@ class ContentRangeError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_content_range(content_range, resumed_from):
|
||||
def parse_content_range(content_range: str, resumed_from: int) -> int:
|
||||
"""
|
||||
Parse and validate Content-Range header.
|
||||
|
||||
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html>
|
||||
<https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html>
|
||||
|
||||
:param content_range: the value of a Content-Range response header
|
||||
eg. "bytes 21010-47021/47022"
|
||||
@ -61,7 +60,7 @@ def parse_content_range(content_range, resumed_from):
|
||||
|
||||
if not match:
|
||||
raise ContentRangeError(
|
||||
'Invalid Content-Range format %r' % content_range)
|
||||
f'Invalid Content-Range format {content_range!r}')
|
||||
|
||||
content_range_dict = match.groupdict()
|
||||
first_byte_pos = int(content_range_dict['first_byte_pos'])
|
||||
@ -78,26 +77,27 @@ def parse_content_range(content_range, resumed_from):
|
||||
# last-byte-pos value, is invalid. The recipient of an invalid
|
||||
# byte-content-range- spec MUST ignore it and any content
|
||||
# transferred along with it."
|
||||
if (first_byte_pos >= last_byte_pos
|
||||
or (instance_length is not None
|
||||
and instance_length <= last_byte_pos)):
|
||||
if (first_byte_pos > last_byte_pos
|
||||
or (instance_length is not None
|
||||
and instance_length <= last_byte_pos)):
|
||||
raise ContentRangeError(
|
||||
'Invalid Content-Range returned: %r' % content_range)
|
||||
f'Invalid Content-Range returned: {content_range!r}')
|
||||
|
||||
if (first_byte_pos != resumed_from
|
||||
or (instance_length is not None
|
||||
and last_byte_pos + 1 != instance_length)):
|
||||
or (instance_length is not None
|
||||
and last_byte_pos + 1 != instance_length)):
|
||||
# Not what we asked for.
|
||||
raise ContentRangeError(
|
||||
'Unexpected Content-Range returned (%r)'
|
||||
' for the requested Range ("bytes=%d-")'
|
||||
% (content_range, resumed_from)
|
||||
f'Unexpected Content-Range returned ({content_range!r})'
|
||||
f' for the requested Range ("bytes={resumed_from}-")'
|
||||
)
|
||||
|
||||
return last_byte_pos + 1
|
||||
|
||||
|
||||
def filename_from_content_disposition(content_disposition):
|
||||
def filename_from_content_disposition(
|
||||
content_disposition: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract and validate filename from a Content-Disposition header.
|
||||
|
||||
@ -107,7 +107,7 @@ def filename_from_content_disposition(content_disposition):
|
||||
"""
|
||||
# attachment; filename=jakubroztocil-httpie-0.4.1-20-g40bd8f6.tar.gz
|
||||
|
||||
msg = Message('Content-Disposition: %s' % content_disposition)
|
||||
msg = Message(f'Content-Disposition: {content_disposition}')
|
||||
filename = msg.get_filename()
|
||||
if filename:
|
||||
# Basic sanitation.
|
||||
@ -116,7 +116,7 @@ def filename_from_content_disposition(content_disposition):
|
||||
return filename
|
||||
|
||||
|
||||
def filename_from_url(url, content_type):
|
||||
def filename_from_url(url: str, content_type: Optional[str]) -> str:
|
||||
fn = urlsplit(url).path.rstrip('/')
|
||||
fn = os.path.basename(fn) if fn else 'index'
|
||||
if '.' not in fn and content_type:
|
||||
@ -127,7 +127,7 @@ def filename_from_url(url, content_type):
|
||||
else:
|
||||
ext = mimetypes.guess_extension(content_type)
|
||||
|
||||
if ext == '.htm': # Python 3
|
||||
if ext == '.htm':
|
||||
ext = '.html'
|
||||
|
||||
if ext:
|
||||
@ -136,7 +136,7 @@ def filename_from_url(url, content_type):
|
||||
return fn
|
||||
|
||||
|
||||
def trim_filename(filename, max_len):
|
||||
def trim_filename(filename: str, max_len: int) -> str:
|
||||
if len(filename) > max_len:
|
||||
trim_by = len(filename) - max_len
|
||||
name, ext = os.path.splitext(filename)
|
||||
@ -147,32 +147,24 @@ def trim_filename(filename, max_len):
|
||||
return filename
|
||||
|
||||
|
||||
def get_filename_max_length(directory):
|
||||
def get_filename_max_length(directory: str) -> int:
|
||||
max_len = 255
|
||||
try:
|
||||
pathconf = os.pathconf
|
||||
except AttributeError:
|
||||
pass # non-posix
|
||||
else:
|
||||
try:
|
||||
max_len = pathconf(directory, 'PC_NAME_MAX')
|
||||
except OSError as e:
|
||||
if e.errno != errno.EINVAL:
|
||||
raise
|
||||
if hasattr(os, 'pathconf') and 'PC_NAME_MAX' in os.pathconf_names:
|
||||
max_len = os.pathconf(directory, 'PC_NAME_MAX')
|
||||
return max_len
|
||||
|
||||
|
||||
def trim_filename_if_needed(filename, directory='.', extra=0):
|
||||
def trim_filename_if_needed(filename: str, directory='.', extra=0) -> str:
|
||||
max_len = get_filename_max_length(directory) - extra
|
||||
if len(filename) > max_len:
|
||||
filename = trim_filename(filename, max_len)
|
||||
return filename
|
||||
|
||||
|
||||
def get_unique_filename(filename, exists=os.path.exists):
|
||||
def get_unique_filename(filename: str, exists=os.path.exists) -> str:
|
||||
attempt = 0
|
||||
while True:
|
||||
suffix = '-' + str(attempt) if attempt > 0 else ''
|
||||
suffix = f'-{attempt}' if attempt > 0 else ''
|
||||
try_filename = trim_filename_if_needed(filename, extra=len(suffix))
|
||||
try_filename += suffix
|
||||
if not exists(try_filename):
|
||||
@ -180,14 +172,17 @@ def get_unique_filename(filename, exists=os.path.exists):
|
||||
attempt += 1
|
||||
|
||||
|
||||
class Downloader(object):
|
||||
class Downloader:
|
||||
|
||||
def __init__(self, output_file=None,
|
||||
resume=False, progress_file=sys.stderr):
|
||||
def __init__(
|
||||
self,
|
||||
output_file: IO = None,
|
||||
resume: bool = False,
|
||||
progress_file: IO = sys.stderr
|
||||
):
|
||||
"""
|
||||
:param resume: Should the download resume if partial download
|
||||
already exists.
|
||||
:type resume: bool
|
||||
|
||||
:param output_file: The file to store response body in. If not
|
||||
provided, it will be guessed from the response.
|
||||
@ -195,24 +190,21 @@ class Downloader(object):
|
||||
:param progress_file: Where to report download progress.
|
||||
|
||||
"""
|
||||
self.finished = False
|
||||
self.status = DownloadStatus()
|
||||
self._output_file = output_file
|
||||
self._resume = resume
|
||||
self._resumed_from = 0
|
||||
self.finished = False
|
||||
|
||||
self.status = Status()
|
||||
self._progress_reporter = ProgressReporterThread(
|
||||
status=self.status,
|
||||
output=progress_file
|
||||
)
|
||||
|
||||
def pre_request(self, request_headers):
|
||||
def pre_request(self, request_headers: dict):
|
||||
"""Called just before the HTTP request is sent.
|
||||
|
||||
Might alter `request_headers`.
|
||||
|
||||
:type request_headers: dict
|
||||
|
||||
"""
|
||||
# Ask the server not to encode the content so that we can resume, etc.
|
||||
request_headers['Accept-Encoding'] = 'identity'
|
||||
@ -221,16 +213,20 @@ class Downloader(object):
|
||||
if bytes_have:
|
||||
# Set ``Range`` header to resume the download
|
||||
# TODO: Use "If-Range: mtime" to make sure it's fresh?
|
||||
request_headers['Range'] = 'bytes=%d-' % bytes_have
|
||||
request_headers['Range'] = f'bytes={bytes_have}-'
|
||||
self._resumed_from = bytes_have
|
||||
|
||||
def start(self, response):
|
||||
def start(
|
||||
self,
|
||||
initial_url: str,
|
||||
final_response: requests.Response
|
||||
) -> Tuple[RawStream, IO]:
|
||||
"""
|
||||
Initiate and return a stream for `response` body with progress
|
||||
callback attached. Can be called only once.
|
||||
|
||||
:param response: Initiated response object with headers already fetched
|
||||
:type response: requests.models.Response
|
||||
:param initial_url: The original requested URL
|
||||
:param final_response: Initiated response object with headers already fetched
|
||||
|
||||
:return: RawStream, output_file
|
||||
|
||||
@ -238,16 +234,22 @@ class Downloader(object):
|
||||
assert not self.status.time_started
|
||||
|
||||
# FIXME: some servers still might sent Content-Encoding: gzip
|
||||
# <https://github.com/jakubroztocil/httpie/issues/423>
|
||||
# <https://github.com/httpie/httpie/issues/423>
|
||||
try:
|
||||
total_size = int(response.headers['Content-Length'])
|
||||
total_size = int(final_response.headers['Content-Length'])
|
||||
except (KeyError, ValueError, TypeError):
|
||||
total_size = None
|
||||
|
||||
if self._output_file:
|
||||
if self._resume and response.status_code == PARTIAL_CONTENT:
|
||||
if not self._output_file:
|
||||
self._output_file = self._get_output_file_from_response(
|
||||
initial_url=initial_url,
|
||||
final_response=final_response,
|
||||
)
|
||||
else:
|
||||
# `--output, -o` provided
|
||||
if self._resume and final_response.status_code == PARTIAL_CONTENT:
|
||||
total_size = parse_content_range(
|
||||
response.headers.get('Content-Range'),
|
||||
final_response.headers.get('Content-Range'),
|
||||
self._resumed_from
|
||||
)
|
||||
|
||||
@ -256,21 +258,8 @@ class Downloader(object):
|
||||
try:
|
||||
self._output_file.seek(0)
|
||||
self._output_file.truncate()
|
||||
except IOError:
|
||||
except OSError:
|
||||
pass # stdout
|
||||
else:
|
||||
# TODO: Should the filename be taken from response.history[0].url?
|
||||
# Output file not specified. Pick a name that doesn't exist yet.
|
||||
filename = None
|
||||
if 'Content-Disposition' in response.headers:
|
||||
filename = filename_from_content_disposition(
|
||||
response.headers['Content-Disposition'])
|
||||
if not filename:
|
||||
filename = filename_from_url(
|
||||
url=response.url,
|
||||
content_type=response.headers.get('Content-Type'),
|
||||
)
|
||||
self._output_file = open(get_unique_filename(filename), mode='a+b')
|
||||
|
||||
self.status.started(
|
||||
resumed_from=self._resumed_from,
|
||||
@ -278,7 +267,7 @@ class Downloader(object):
|
||||
)
|
||||
|
||||
stream = RawStream(
|
||||
msg=HTTPResponse(response),
|
||||
msg=HTTPResponse(final_response),
|
||||
with_headers=False,
|
||||
with_body=True,
|
||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||
@ -286,12 +275,8 @@ class Downloader(object):
|
||||
)
|
||||
|
||||
self._progress_reporter.output.write(
|
||||
'Downloading %sto "%s"\n' % (
|
||||
(humanize_bytes(total_size) + ' '
|
||||
if total_size is not None
|
||||
else ''),
|
||||
self._output_file.name
|
||||
)
|
||||
f'Downloading {humanize_bytes(total_size) + " " if total_size is not None else ""}'
|
||||
f'to "{self._output_file.name}"\n'
|
||||
)
|
||||
self._progress_reporter.start()
|
||||
|
||||
@ -306,27 +291,44 @@ class Downloader(object):
|
||||
self._progress_reporter.stop()
|
||||
|
||||
@property
|
||||
def interrupted(self):
|
||||
def interrupted(self) -> bool:
|
||||
return (
|
||||
self.finished
|
||||
and self.status.total_size
|
||||
and self.status.total_size != self.status.downloaded
|
||||
)
|
||||
|
||||
def chunk_downloaded(self, chunk):
|
||||
def chunk_downloaded(self, chunk: bytes):
|
||||
"""
|
||||
A download progress callback.
|
||||
|
||||
:param chunk: A chunk of response body data that has just
|
||||
been downloaded and written to the output.
|
||||
:type chunk: bytes
|
||||
|
||||
"""
|
||||
self.status.chunk_downloaded(len(chunk))
|
||||
|
||||
@staticmethod
|
||||
def _get_output_file_from_response(
|
||||
initial_url: str,
|
||||
final_response: requests.Response,
|
||||
) -> IO:
|
||||
# Output file not specified. Pick a name that doesn't exist yet.
|
||||
filename = None
|
||||
if 'Content-Disposition' in final_response.headers:
|
||||
filename = filename_from_content_disposition(
|
||||
final_response.headers['Content-Disposition'])
|
||||
if not filename:
|
||||
filename = filename_from_url(
|
||||
url=initial_url,
|
||||
content_type=final_response.headers.get('Content-Type'),
|
||||
)
|
||||
unique_filename = get_unique_filename(filename)
|
||||
return open(unique_filename, mode='a+b')
|
||||
|
||||
class Status(object):
|
||||
"""Holds details about the downland status."""
|
||||
|
||||
class DownloadStatus:
|
||||
"""Holds details about the download status."""
|
||||
|
||||
def __init__(self):
|
||||
self.downloaded = 0
|
||||
@ -339,7 +341,7 @@ class Status(object):
|
||||
assert self.time_started is None
|
||||
self.total_size = total_size
|
||||
self.downloaded = self.resumed_from = resumed_from
|
||||
self.time_started = time()
|
||||
self.time_started = monotonic()
|
||||
|
||||
def chunk_downloaded(self, size):
|
||||
assert self.time_finished is None
|
||||
@ -352,7 +354,7 @@ class Status(object):
|
||||
def finished(self):
|
||||
assert self.time_started is not None
|
||||
assert self.time_finished is None
|
||||
self.time_finished = time()
|
||||
self.time_finished = monotonic()
|
||||
|
||||
|
||||
class ProgressReporterThread(threading.Thread):
|
||||
@ -362,13 +364,15 @@ class ProgressReporterThread(threading.Thread):
|
||||
Uses threading to periodically update the status (speed, ETA, etc.).
|
||||
|
||||
"""
|
||||
def __init__(self, status, output, tick=.1, update_interval=1):
|
||||
"""
|
||||
|
||||
:type status: Status
|
||||
:type output: file
|
||||
"""
|
||||
super(ProgressReporterThread, self).__init__()
|
||||
def __init__(
|
||||
self,
|
||||
status: DownloadStatus,
|
||||
output: IO,
|
||||
tick=.1,
|
||||
update_interval=1
|
||||
):
|
||||
super().__init__()
|
||||
self.status = status
|
||||
self.output = output
|
||||
self._tick = tick
|
||||
@ -376,7 +380,7 @@ class ProgressReporterThread(threading.Thread):
|
||||
self._spinner_pos = 0
|
||||
self._status_line = ''
|
||||
self._prev_bytes = 0
|
||||
self._prev_time = time()
|
||||
self._prev_time = monotonic()
|
||||
self._should_stop = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
@ -393,16 +397,11 @@ class ProgressReporterThread(threading.Thread):
|
||||
sleep(self._tick)
|
||||
|
||||
def report_speed(self):
|
||||
|
||||
now = time()
|
||||
|
||||
now = monotonic()
|
||||
if now - self._prev_time >= self._update_interval:
|
||||
downloaded = self.status.downloaded
|
||||
try:
|
||||
speed = ((downloaded - self._prev_bytes)
|
||||
/ (now - self._prev_time))
|
||||
except ZeroDivisionError:
|
||||
speed = 0
|
||||
speed = ((downloaded - self._prev_bytes)
|
||||
/ (now - self._prev_time))
|
||||
|
||||
if not self.status.total_size:
|
||||
self._status_line = PROGRESS_NO_CONTENT_LENGTH.format(
|
||||
@ -410,10 +409,9 @@ class ProgressReporterThread(threading.Thread):
|
||||
speed=humanize_bytes(speed),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
percentage = downloaded / self.status.total_size * 100
|
||||
except ZeroDivisionError:
|
||||
percentage = 0
|
||||
percentage = (downloaded / self.status.total_size * 100
|
||||
if self.status.total_size
|
||||
else 0)
|
||||
|
||||
if not speed:
|
||||
eta = '-:--:--'
|
||||
@ -421,7 +419,7 @@ class ProgressReporterThread(threading.Thread):
|
||||
s = int((self.status.total_size - downloaded) / speed)
|
||||
h, s = divmod(s, 60 * 60)
|
||||
m, s = divmod(s, 60)
|
||||
eta = '{0}:{1:0>2}:{2:0>2}'.format(h, m, s)
|
||||
eta = f'{h}:{m:0>2}:{s:0>2}'
|
||||
|
||||
self._status_line = PROGRESS.format(
|
||||
percentage=percentage,
|
||||
@ -434,33 +432,20 @@ class ProgressReporterThread(threading.Thread):
|
||||
self._prev_bytes = downloaded
|
||||
|
||||
self.output.write(
|
||||
CLEAR_LINE
|
||||
+ ' '
|
||||
+ SPINNER[self._spinner_pos]
|
||||
+ ' '
|
||||
+ self._status_line
|
||||
f'{CLEAR_LINE} {SPINNER[self._spinner_pos]} {self._status_line}'
|
||||
)
|
||||
self.output.flush()
|
||||
|
||||
self._spinner_pos = (self._spinner_pos + 1
|
||||
if self._spinner_pos + 1 != len(SPINNER)
|
||||
else 0)
|
||||
self._spinner_pos = (self._spinner_pos + 1) % len(SPINNER)
|
||||
|
||||
def sum_up(self):
|
||||
actually_downloaded = (
|
||||
self.status.downloaded - self.status.resumed_from)
|
||||
time_taken = self.status.time_finished - self.status.time_started
|
||||
speed = actually_downloaded / time_taken if time_taken else actually_downloaded
|
||||
|
||||
self.output.write(CLEAR_LINE)
|
||||
|
||||
try:
|
||||
speed = actually_downloaded / time_taken
|
||||
except ZeroDivisionError:
|
||||
# Either time is 0 (not all systems provide `time.time`
|
||||
# with a better precision than 1 second), and/or nothing
|
||||
# has been downloaded.
|
||||
speed = actually_downloaded
|
||||
|
||||
self.output.write(SUMMARY.format(
|
||||
downloaded=humanize_bytes(actually_downloaded),
|
||||
total=(self.status.total_size
|
||||
|
50
httpie/encoding.py
Normal file
50
httpie/encoding.py
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import Union
|
||||
|
||||
from charset_normalizer import from_bytes
|
||||
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
|
||||
|
||||
UTF8 = 'utf-8'
|
||||
|
||||
ContentBytes = Union[bytearray, bytes]
|
||||
|
||||
|
||||
def detect_encoding(content: ContentBytes) -> str:
|
||||
"""
|
||||
We default to UTF-8 if text too short, because the detection
|
||||
can return a random encoding leading to confusing results
|
||||
given the `charset_normalizer` version (< 2.0.5).
|
||||
|
||||
>>> too_short = ']"foo"'
|
||||
>>> detected = from_bytes(too_short.encode()).best().encoding
|
||||
>>> detected
|
||||
'ascii'
|
||||
>>> too_short.encode().decode(detected)
|
||||
']"foo"'
|
||||
"""
|
||||
encoding = UTF8
|
||||
if len(content) > TOO_SMALL_SEQUENCE:
|
||||
match = from_bytes(bytes(content)).best()
|
||||
if match:
|
||||
encoding = match.encoding
|
||||
return encoding
|
||||
|
||||
|
||||
def smart_decode(content: ContentBytes, encoding: str) -> str:
|
||||
"""Decode `content` using the given `encoding`.
|
||||
If no `encoding` is provided, the best effort is to guess it from `content`.
|
||||
|
||||
Unicode errors are replaced.
|
||||
|
||||
"""
|
||||
if not encoding:
|
||||
encoding = detect_encoding(content)
|
||||
return content.decode(encoding, 'replace')
|
||||
|
||||
|
||||
def smart_encode(content: str, encoding: str) -> bytes:
|
||||
"""Encode `content` using the given `encoding`.
|
||||
|
||||
Unicode errors are replaced.
|
||||
|
||||
"""
|
||||
return content.encode(encoding, 'replace')
|
758
httpie/input.py
758
httpie/input.py
@ -1,758 +0,0 @@
|
||||
"""Parsing and processing of CLI input (args, auth credentials, files, stdin).
|
||||
|
||||
"""
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import re
|
||||
import errno
|
||||
import mimetypes
|
||||
import getpass
|
||||
from io import BytesIO
|
||||
from collections import namedtuple, Iterable, OrderedDict
|
||||
# noinspection PyCompatibility
|
||||
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
|
||||
|
||||
# TODO: Use MultiDict for headers once added to `requests`.
|
||||
# https://github.com/jakubroztocil/httpie/issues/130
|
||||
from httpie.plugins import plugin_manager
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from httpie.compat import urlsplit, str, is_pypy, is_py27
|
||||
from httpie.sessions import VALID_SESSION_NAME_PATTERN
|
||||
from httpie.utils import load_json_preserve_order
|
||||
|
||||
|
||||
# ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
# <http://tools.ietf.org/html/rfc3986#section-3.1>
|
||||
URL_SCHEME_RE = re.compile(r'^[a-z][a-z0-9.+-]*://', re.IGNORECASE)
|
||||
|
||||
HTTP_POST = 'POST'
|
||||
HTTP_GET = 'GET'
|
||||
|
||||
|
||||
# Various separators used in args
|
||||
SEP_HEADERS = ':'
|
||||
SEP_HEADERS_EMPTY = ';'
|
||||
SEP_CREDENTIALS = ':'
|
||||
SEP_PROXY = ':'
|
||||
SEP_DATA = '='
|
||||
SEP_DATA_RAW_JSON = ':='
|
||||
SEP_FILES = '@'
|
||||
SEP_DATA_EMBED_FILE = '=@'
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE = ':=@'
|
||||
SEP_QUERY = '=='
|
||||
|
||||
# Separators that become request data
|
||||
SEP_GROUP_DATA_ITEMS = frozenset([
|
||||
SEP_DATA,
|
||||
SEP_DATA_RAW_JSON,
|
||||
SEP_FILES,
|
||||
SEP_DATA_EMBED_FILE,
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE
|
||||
])
|
||||
|
||||
# Separators for items whose value is a filename to be embedded
|
||||
SEP_GROUP_DATA_EMBED_ITEMS = frozenset([
|
||||
SEP_DATA_EMBED_FILE,
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
# Separators for raw JSON items
|
||||
SEP_GROUP_RAW_JSON_ITEMS = frozenset([
|
||||
SEP_DATA_RAW_JSON,
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
# Separators allowed in ITEM arguments
|
||||
SEP_GROUP_ALL_ITEMS = frozenset([
|
||||
SEP_HEADERS,
|
||||
SEP_HEADERS_EMPTY,
|
||||
SEP_QUERY,
|
||||
SEP_DATA,
|
||||
SEP_DATA_RAW_JSON,
|
||||
SEP_FILES,
|
||||
SEP_DATA_EMBED_FILE,
|
||||
SEP_DATA_EMBED_RAW_JSON_FILE,
|
||||
])
|
||||
|
||||
|
||||
# Output options
|
||||
OUT_REQ_HEAD = 'H'
|
||||
OUT_REQ_BODY = 'B'
|
||||
OUT_RESP_HEAD = 'h'
|
||||
OUT_RESP_BODY = 'b'
|
||||
|
||||
OUTPUT_OPTIONS = frozenset([
|
||||
OUT_REQ_HEAD,
|
||||
OUT_REQ_BODY,
|
||||
OUT_RESP_HEAD,
|
||||
OUT_RESP_BODY
|
||||
])
|
||||
|
||||
# Pretty
|
||||
PRETTY_MAP = {
|
||||
'all': ['format', 'colors'],
|
||||
'colors': ['colors'],
|
||||
'format': ['format'],
|
||||
'none': []
|
||||
}
|
||||
PRETTY_STDOUT_TTY_ONLY = object()
|
||||
|
||||
|
||||
# Defaults
|
||||
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
|
||||
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
|
||||
|
||||
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
'ssl2.3': 'PROTOCOL_SSLv23',
|
||||
'ssl3': 'PROTOCOL_SSLv3',
|
||||
'tls1': 'PROTOCOL_TLSv1',
|
||||
'tls1.1': 'PROTOCOL_TLSv1_1',
|
||||
'tls1.2': 'PROTOCOL_TLSv1_2',
|
||||
'tls1.3': 'PROTOCOL_TLSv1_3',
|
||||
}
|
||||
SSL_VERSION_ARG_MAPPING = {
|
||||
cli_arg: getattr(ssl, ssl_constant)
|
||||
for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
|
||||
if hasattr(ssl, ssl_constant)
|
||||
}
|
||||
|
||||
|
||||
class HTTPieArgumentParser(ArgumentParser):
|
||||
"""Adds additional logic to `argparse.ArgumentParser`.
|
||||
|
||||
Handles all input (CLI args, file args, stdin), applies defaults,
|
||||
and performs extra validation.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['add_help'] = False
|
||||
super(HTTPieArgumentParser, self).__init__(*args, **kwargs)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def parse_args(self, env, args=None, namespace=None):
|
||||
|
||||
self.env = env
|
||||
self.args, no_options = super(HTTPieArgumentParser, self)\
|
||||
.parse_known_args(args, namespace)
|
||||
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
|
||||
# Arguments processing and environment setup.
|
||||
self._apply_no_options(no_options)
|
||||
self._validate_download_options()
|
||||
self._setup_standard_streams()
|
||||
self._process_output_options()
|
||||
self._process_pretty_options()
|
||||
self._guess_method()
|
||||
self._parse_items()
|
||||
if not self.args.ignore_stdin and not env.stdin_isatty:
|
||||
self._body_from_file(self.env.stdin)
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
scheme = self.args.default_scheme + "://"
|
||||
|
||||
# See if we're using curl style shorthand for localhost (:3000/foo)
|
||||
shorthand = re.match(r'^:(?!:)(\d*)(/?.*)$', self.args.url)
|
||||
if shorthand:
|
||||
port = shorthand.group(1)
|
||||
rest = shorthand.group(2)
|
||||
self.args.url = scheme + 'localhost'
|
||||
if port:
|
||||
self.args.url += ':' + port
|
||||
self.args.url += rest
|
||||
else:
|
||||
self.args.url = scheme + self.args.url
|
||||
self._process_auth()
|
||||
|
||||
return self.args
|
||||
|
||||
# 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(HTTPieArgumentParser, self)._print_message(message, file)
|
||||
|
||||
def _setup_standard_streams(self):
|
||||
"""
|
||||
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
|
||||
|
||||
"""
|
||||
self.args.output_file_specified = bool(self.args.output_file)
|
||||
if self.args.download:
|
||||
# FIXME: Come up with a cleaner solution.
|
||||
if not self.args.output_file and not self.env.stdout_isatty:
|
||||
# Use stdout as the download output file.
|
||||
self.args.output_file = self.env.stdout
|
||||
# With `--download`, we write everything that would normally go to
|
||||
# `stdout` to `stderr` instead. Let's replace the stream so that
|
||||
# we don't have to use many `if`s throughout the codebase.
|
||||
# The response body will be treated separately.
|
||||
self.env.stdout = self.env.stderr
|
||||
self.env.stdout_isatty = self.env.stderr_isatty
|
||||
elif self.args.output_file:
|
||||
# When not `--download`ing, then `--output` simply replaces
|
||||
# `stdout`. The file is opened for appending, which isn't what
|
||||
# we want in this case.
|
||||
self.args.output_file.seek(0)
|
||||
try:
|
||||
self.args.output_file.truncate()
|
||||
except IOError as e:
|
||||
if e.errno == errno.EINVAL:
|
||||
# E.g. /dev/null on Linux.
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
self.env.stdout = self.args.output_file
|
||||
self.env.stdout_isatty = False
|
||||
|
||||
def _process_auth(self):
|
||||
# TODO: refactor
|
||||
self.args.auth_plugin = None
|
||||
default_auth_plugin = plugin_manager.get_auth_plugins()[0]
|
||||
auth_type_set = self.args.auth_type is not None
|
||||
url = urlsplit(self.args.url)
|
||||
|
||||
if self.args.auth is None and not auth_type_set:
|
||||
if url.username is not None:
|
||||
# Handle http://username:password@hostname/
|
||||
username = url.username
|
||||
password = url.password or ''
|
||||
self.args.auth = AuthCredentials(
|
||||
key=username,
|
||||
value=password,
|
||||
sep=SEP_CREDENTIALS,
|
||||
orig=SEP_CREDENTIALS.join([username, password])
|
||||
)
|
||||
|
||||
if self.args.auth is not None or auth_type_set:
|
||||
if not self.args.auth_type:
|
||||
self.args.auth_type = default_auth_plugin.auth_type
|
||||
plugin = plugin_manager.get_auth_plugin(self.args.auth_type)()
|
||||
|
||||
if plugin.auth_require and self.args.auth is None:
|
||||
self.error('--auth required')
|
||||
|
||||
plugin.raw_auth = self.args.auth
|
||||
self.args.auth_plugin = plugin
|
||||
already_parsed = isinstance(self.args.auth, AuthCredentials)
|
||||
|
||||
if self.args.auth is None or not plugin.auth_parse:
|
||||
self.args.auth = plugin.get_auth()
|
||||
else:
|
||||
if already_parsed:
|
||||
# from the URL
|
||||
credentials = self.args.auth
|
||||
else:
|
||||
credentials = parse_auth(self.args.auth)
|
||||
|
||||
if (not credentials.has_password()
|
||||
and plugin.prompt_password):
|
||||
if self.args.ignore_stdin:
|
||||
# Non-tty stdin read by now
|
||||
self.error(
|
||||
'Unable to prompt for passwords because'
|
||||
' --ignore-stdin is set.'
|
||||
)
|
||||
credentials.prompt_password(url.netloc)
|
||||
self.args.auth = plugin.get_auth(
|
||||
username=credentials.key,
|
||||
password=credentials.value,
|
||||
)
|
||||
|
||||
def _apply_no_options(self, no_options):
|
||||
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
|
||||
its default value. This allows for un-setting of options, e.g.,
|
||||
specified in config.
|
||||
|
||||
"""
|
||||
invalid = []
|
||||
|
||||
for option in no_options:
|
||||
if not option.startswith('--no-'):
|
||||
invalid.append(option)
|
||||
continue
|
||||
|
||||
# --no-option => --option
|
||||
inverted = '--' + option[5:]
|
||||
for action in self._actions:
|
||||
if inverted in action.option_strings:
|
||||
setattr(self.args, action.dest, action.default)
|
||||
break
|
||||
else:
|
||||
invalid.append(option)
|
||||
|
||||
if invalid:
|
||||
msg = 'unrecognized arguments: %s'
|
||||
self.error(msg % ' '.join(invalid))
|
||||
|
||||
def _body_from_file(self, fd):
|
||||
"""There can only be one source of request data.
|
||||
|
||||
Bytes are always read.
|
||||
|
||||
"""
|
||||
if self.args.data:
|
||||
self.error('Request body (from stdin or a file) and request '
|
||||
'data (key=value) cannot be mixed. Pass '
|
||||
'--ignore-stdin to let key/value take priority.')
|
||||
self.args.data = getattr(fd, 'buffer', fd).read()
|
||||
|
||||
def _guess_method(self):
|
||||
"""Set `args.method` if not specified to either POST or GET
|
||||
based on whether the request has data or not.
|
||||
|
||||
"""
|
||||
if self.args.method is None:
|
||||
# Invoked as `http URL'.
|
||||
assert not self.args.items
|
||||
if not self.args.ignore_stdin and not self.env.stdin_isatty:
|
||||
self.args.method = HTTP_POST
|
||||
else:
|
||||
self.args.method = HTTP_GET
|
||||
|
||||
# FIXME: False positive, e.g., "localhost" matches but is a valid URL.
|
||||
elif not re.match('^[a-zA-Z]+$', self.args.method):
|
||||
# Invoked as `http URL item+'. The URL is now in `args.method`
|
||||
# and the first ITEM is now incorrectly in `args.url`.
|
||||
try:
|
||||
# Parse the URL as an ITEM and store it as the first ITEM arg.
|
||||
self.args.items.insert(0, KeyValueArgType(
|
||||
*SEP_GROUP_ALL_ITEMS).__call__(self.args.url))
|
||||
|
||||
except ArgumentTypeError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
|
||||
else:
|
||||
# Set the URL correctly
|
||||
self.args.url = self.args.method
|
||||
# Infer the method
|
||||
has_data = (
|
||||
(not self.args.ignore_stdin and not self.env.stdin_isatty)
|
||||
or any(
|
||||
item.sep in SEP_GROUP_DATA_ITEMS
|
||||
for item in self.args.items
|
||||
)
|
||||
)
|
||||
self.args.method = HTTP_POST if has_data else HTTP_GET
|
||||
|
||||
def _parse_items(self):
|
||||
"""Parse `args.items` into `args.headers`, `args.data`, `args.params`,
|
||||
and `args.files`.
|
||||
|
||||
"""
|
||||
try:
|
||||
items = parse_items(
|
||||
items=self.args.items,
|
||||
data_class=ParamsDict if self.args.form else OrderedDict
|
||||
)
|
||||
except ParseError as e:
|
||||
if self.args.traceback:
|
||||
raise
|
||||
self.error(e.args[0])
|
||||
else:
|
||||
self.args.headers = items.headers
|
||||
self.args.data = items.data
|
||||
self.args.files = items.files
|
||||
self.args.params = items.params
|
||||
|
||||
if self.args.files and not self.args.form:
|
||||
# `http url @/path/to/file`
|
||||
file_fields = list(self.args.files.keys())
|
||||
if file_fields != ['']:
|
||||
self.error(
|
||||
'Invalid file fields (perhaps you meant --form?): %s'
|
||||
% ','.join(file_fields))
|
||||
|
||||
fn, fd, ct = self.args.files['']
|
||||
self.args.files = {}
|
||||
|
||||
self._body_from_file(fd)
|
||||
|
||||
if 'Content-Type' not in self.args.headers:
|
||||
content_type = get_content_type(fn)
|
||||
if content_type:
|
||||
self.args.headers['Content-Type'] = content_type
|
||||
|
||||
def _process_output_options(self):
|
||||
"""Apply defaults to output options, or validate the provided ones.
|
||||
|
||||
The default output options are stdout-type-sensitive.
|
||||
|
||||
"""
|
||||
def check_options(value, option):
|
||||
unknown = set(value) - OUTPUT_OPTIONS
|
||||
if unknown:
|
||||
self.error('Unknown output options: {0}={1}'.format(
|
||||
option,
|
||||
','.join(unknown)
|
||||
))
|
||||
|
||||
if self.args.verbose:
|
||||
self.args.all = True
|
||||
|
||||
if self.args.output_options is None:
|
||||
if self.args.verbose:
|
||||
self.args.output_options = ''.join(OUTPUT_OPTIONS)
|
||||
else:
|
||||
self.args.output_options = (
|
||||
OUTPUT_OPTIONS_DEFAULT
|
||||
if self.env.stdout_isatty
|
||||
else OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED
|
||||
)
|
||||
|
||||
if self.args.output_options_history is None:
|
||||
self.args.output_options_history = self.args.output_options
|
||||
|
||||
check_options(self.args.output_options, '--print')
|
||||
check_options(self.args.output_options_history, '--history-print')
|
||||
|
||||
if self.args.download and OUT_RESP_BODY in self.args.output_options:
|
||||
# Response body is always downloaded with --download and it goes
|
||||
# through a different routine, so we remove it.
|
||||
self.args.output_options = str(
|
||||
set(self.args.output_options) - set(OUT_RESP_BODY))
|
||||
|
||||
def _process_pretty_options(self):
|
||||
if self.args.prettify == PRETTY_STDOUT_TTY_ONLY:
|
||||
self.args.prettify = PRETTY_MAP[
|
||||
'all' if self.env.stdout_isatty else 'none']
|
||||
elif (self.args.prettify and self.env.is_windows
|
||||
and self.args.output_file):
|
||||
self.error('Only terminal output can be colorized on Windows.')
|
||||
else:
|
||||
# noinspection PyTypeChecker
|
||||
self.args.prettify = PRETTY_MAP[self.args.prettify]
|
||||
|
||||
def _validate_download_options(self):
|
||||
if not self.args.download:
|
||||
if self.args.download_resume:
|
||||
self.error('--continue only works with --download')
|
||||
if self.args.download_resume and not (
|
||||
self.args.download and self.args.output_file):
|
||||
self.error('--continue requires --output to be specified')
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KeyValue(object):
|
||||
"""Base key-value pair parsed from CLI."""
|
||||
|
||||
def __init__(self, key, value, sep, orig):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.sep = sep
|
||||
self.orig = orig
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
|
||||
class SessionNameValidator(object):
|
||||
|
||||
def __init__(self, error_message):
|
||||
self.error_message = error_message
|
||||
|
||||
def __call__(self, value):
|
||||
# Session name can be a path or just a name.
|
||||
if (os.path.sep not in value
|
||||
and not VALID_SESSION_NAME_PATTERN.search(value)):
|
||||
raise ArgumentError(None, self.error_message)
|
||||
return value
|
||||
|
||||
|
||||
class KeyValueArgType(object):
|
||||
"""A key-value pair argument type used with `argparse`.
|
||||
|
||||
Parses a key-value arg and constructs a `KeyValue` instance.
|
||||
Used for headers, form data, and other key-value pair types.
|
||||
|
||||
"""
|
||||
|
||||
key_value_class = KeyValue
|
||||
|
||||
def __init__(self, *separators):
|
||||
self.separators = separators
|
||||
self.special_characters = set('\\')
|
||||
for separator in separators:
|
||||
self.special_characters.update(separator)
|
||||
|
||||
def __call__(self, string):
|
||||
"""Parse `string` and return `self.key_value_class()` instance.
|
||||
|
||||
The best of `self.separators` is determined (first found, longest).
|
||||
Back slash escaped characters aren't considered as separators
|
||||
(or parts thereof). Literal back slash characters have to be escaped
|
||||
as well (r'\\').
|
||||
|
||||
"""
|
||||
|
||||
class Escaped(str):
|
||||
"""Represents an escaped character."""
|
||||
|
||||
def tokenize(string):
|
||||
r"""Tokenize `string`. There are only two token types - strings
|
||||
and escaped characters:
|
||||
|
||||
tokenize(r'foo\=bar\\baz')
|
||||
=> ['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
|
||||
|
||||
"""
|
||||
tokens = ['']
|
||||
characters = iter(string)
|
||||
for char in characters:
|
||||
if char == '\\':
|
||||
char = next(characters, '')
|
||||
if char not in self.special_characters:
|
||||
tokens[-1] += '\\' + char
|
||||
else:
|
||||
tokens.extend([Escaped(char), ''])
|
||||
else:
|
||||
tokens[-1] += char
|
||||
return tokens
|
||||
|
||||
tokens = tokenize(string)
|
||||
|
||||
# Sorting by length ensures that the longest one will be
|
||||
# chosen as it will overwrite any shorter ones starting
|
||||
# at the same position in the `found` dictionary.
|
||||
separators = sorted(self.separators, key=len)
|
||||
|
||||
for i, token in enumerate(tokens):
|
||||
|
||||
if isinstance(token, Escaped):
|
||||
continue
|
||||
|
||||
found = {}
|
||||
for sep in separators:
|
||||
pos = token.find(sep)
|
||||
if pos != -1:
|
||||
found[pos] = sep
|
||||
|
||||
if found:
|
||||
# Starting first, longest separator found.
|
||||
sep = found[min(found.keys())]
|
||||
|
||||
key, value = token.split(sep, 1)
|
||||
|
||||
# Any preceding tokens are part of the key.
|
||||
key = ''.join(tokens[:i]) + key
|
||||
|
||||
# Any following tokens are part of the value.
|
||||
value += ''.join(tokens[i + 1:])
|
||||
|
||||
break
|
||||
|
||||
else:
|
||||
raise ArgumentTypeError(
|
||||
u'"%s" is not a valid value' % string)
|
||||
|
||||
return self.key_value_class(
|
||||
key=key, value=value, sep=sep, orig=string)
|
||||
|
||||
|
||||
class AuthCredentials(KeyValue):
|
||||
"""Represents parsed credentials."""
|
||||
|
||||
def _getpass(self, prompt):
|
||||
# To allow mocking.
|
||||
return getpass.getpass(str(prompt))
|
||||
|
||||
def has_password(self):
|
||||
return self.value is not None
|
||||
|
||||
def prompt_password(self, host):
|
||||
try:
|
||||
self.value = self._getpass(
|
||||
'http: password for %s@%s: ' % (self.key, host))
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
class AuthCredentialsArgType(KeyValueArgType):
|
||||
"""A key-value arg type that parses credentials."""
|
||||
|
||||
key_value_class = AuthCredentials
|
||||
|
||||
def __call__(self, string):
|
||||
"""Parse credentials from `string`.
|
||||
|
||||
("username" or "username:password").
|
||||
|
||||
"""
|
||||
try:
|
||||
return super(AuthCredentialsArgType, self).__call__(string)
|
||||
except ArgumentTypeError:
|
||||
# No password provided, will prompt for it later.
|
||||
return self.key_value_class(
|
||||
key=string,
|
||||
value=None,
|
||||
sep=SEP_CREDENTIALS,
|
||||
orig=string
|
||||
)
|
||||
|
||||
|
||||
parse_auth = AuthCredentialsArgType(SEP_CREDENTIALS)
|
||||
|
||||
|
||||
class RequestItemsDict(OrderedDict):
|
||||
"""Multi-value dict for URL parameters and form data."""
|
||||
|
||||
if is_pypy and is_py27:
|
||||
# Manually set keys when initialized with an iterable as PyPy
|
||||
# doesn't call __setitem__ in such case (pypy3 does).
|
||||
def __init__(self, *args, **kwargs):
|
||||
if len(args) == 1 and isinstance(args[0], Iterable):
|
||||
super(RequestItemsDict, self).__init__(**kwargs)
|
||||
for k, v in args[0]:
|
||||
self[k] = v
|
||||
else:
|
||||
super(RequestItemsDict, self).__init__(*args, **kwargs)
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
def __setitem__(self, key, value):
|
||||
""" If `key` is assigned more than once, `self[key]` holds a
|
||||
`list` of all the values.
|
||||
|
||||
This allows having multiple fields with the same name in form
|
||||
data and URL params.
|
||||
|
||||
"""
|
||||
assert not isinstance(value, list)
|
||||
if key not in self:
|
||||
super(RequestItemsDict, self).__setitem__(key, value)
|
||||
else:
|
||||
if not isinstance(self[key], list):
|
||||
super(RequestItemsDict, self).__setitem__(key, [self[key]])
|
||||
self[key].append(value)
|
||||
|
||||
|
||||
class ParamsDict(RequestItemsDict):
|
||||
pass
|
||||
|
||||
|
||||
class DataDict(RequestItemsDict):
|
||||
|
||||
def items(self):
|
||||
for key, values in super(RequestItemsDict, self).items():
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for value in values:
|
||||
yield key, value
|
||||
|
||||
|
||||
RequestItems = namedtuple('RequestItems',
|
||||
['headers', 'data', 'files', 'params'])
|
||||
|
||||
|
||||
def get_content_type(filename):
|
||||
"""
|
||||
Return the content type for ``filename`` in format appropriate
|
||||
for Content-Type headers, or ``None`` if the file type is unknown
|
||||
to ``mimetypes``.
|
||||
|
||||
"""
|
||||
mime, encoding = mimetypes.guess_type(filename, strict=False)
|
||||
if mime:
|
||||
content_type = mime
|
||||
if encoding:
|
||||
content_type = '%s; charset=%s' % (mime, encoding)
|
||||
return content_type
|
||||
|
||||
|
||||
def parse_items(items,
|
||||
headers_class=CaseInsensitiveDict,
|
||||
data_class=OrderedDict,
|
||||
files_class=DataDict,
|
||||
params_class=ParamsDict):
|
||||
"""Parse `KeyValue` `items` into `data`, `headers`, `files`,
|
||||
and `params`.
|
||||
|
||||
"""
|
||||
headers = []
|
||||
data = []
|
||||
files = []
|
||||
params = []
|
||||
for item in items:
|
||||
value = item.value
|
||||
if item.sep == SEP_HEADERS:
|
||||
if value == '':
|
||||
# No value => unset the header
|
||||
value = None
|
||||
target = headers
|
||||
elif item.sep == SEP_HEADERS_EMPTY:
|
||||
if item.value:
|
||||
raise ParseError(
|
||||
'Invalid item "%s" '
|
||||
'(to specify an empty header use `Header;`)'
|
||||
% item.orig
|
||||
)
|
||||
target = headers
|
||||
elif item.sep == SEP_QUERY:
|
||||
target = params
|
||||
elif item.sep == SEP_FILES:
|
||||
try:
|
||||
with open(os.path.expanduser(value), 'rb') as f:
|
||||
value = (os.path.basename(value),
|
||||
BytesIO(f.read()),
|
||||
get_content_type(value))
|
||||
except IOError as e:
|
||||
raise ParseError('"%s": %s' % (item.orig, e))
|
||||
target = files
|
||||
|
||||
elif item.sep in SEP_GROUP_DATA_ITEMS:
|
||||
|
||||
if item.sep in SEP_GROUP_DATA_EMBED_ITEMS:
|
||||
try:
|
||||
with open(os.path.expanduser(value), 'rb') as f:
|
||||
value = f.read().decode('utf8')
|
||||
except IOError as e:
|
||||
raise ParseError('"%s": %s' % (item.orig, e))
|
||||
except UnicodeDecodeError:
|
||||
raise ParseError(
|
||||
'"%s": cannot embed the content of "%s",'
|
||||
' not a UTF8 or ASCII-encoded text file'
|
||||
% (item.orig, item.value)
|
||||
)
|
||||
|
||||
if item.sep in SEP_GROUP_RAW_JSON_ITEMS:
|
||||
try:
|
||||
value = load_json_preserve_order(value)
|
||||
except ValueError as e:
|
||||
raise ParseError('"%s": %s' % (item.orig, e))
|
||||
target = data
|
||||
|
||||
else:
|
||||
raise TypeError(item)
|
||||
|
||||
target.append((item.key, value))
|
||||
|
||||
return RequestItems(headers_class(headers),
|
||||
data_class(data),
|
||||
files_class(files),
|
||||
params_class(params))
|
||||
|
||||
|
||||
def readable_file_arg(filename):
|
||||
try:
|
||||
open(filename, 'rb')
|
||||
except IOError as ex:
|
||||
raise ArgumentTypeError('%s: %s' % (filename, ex.args[1]))
|
||||
return filename
|
101
httpie/models.py
101
httpie/models.py
@ -1,41 +1,40 @@
|
||||
from httpie.compat import urlsplit, str
|
||||
from typing import Iterable
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from .utils import split_cookies, parse_content_type_header
|
||||
from .compat import cached_property
|
||||
|
||||
|
||||
class HTTPMessage(object):
|
||||
class HTTPMessage:
|
||||
"""Abstract class for HTTP messages."""
|
||||
|
||||
def __init__(self, orig):
|
||||
self._orig = orig
|
||||
|
||||
def iter_body(self, chunk_size):
|
||||
def iter_body(self, chunk_size: int) -> Iterable[bytes]:
|
||||
"""Return an iterator over the body."""
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError
|
||||
|
||||
def iter_lines(self, chunk_size):
|
||||
def iter_lines(self, chunk_size: int) -> Iterable[bytes]:
|
||||
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
def headers(self) -> str:
|
||||
"""Return a `str` with the message's headers."""
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError
|
||||
|
||||
@cached_property
|
||||
def encoding(self) -> str:
|
||||
ct, params = parse_content_type_header(self.content_type)
|
||||
return params.get('charset', '')
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
"""Return a `str` with the message's encoding, if known."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
"""Return a `bytes` with the message's body."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
def content_type(self) -> str:
|
||||
"""Return the message content type."""
|
||||
ct = self._orig.headers.get('Content-Type', '')
|
||||
if not isinstance(ct, str):
|
||||
ct = ct.decode('utf8')
|
||||
ct = ct.decode()
|
||||
return ct
|
||||
|
||||
|
||||
@ -51,43 +50,32 @@ class HTTPResponse(HTTPMessage):
|
||||
# noinspection PyProtectedMember
|
||||
@property
|
||||
def headers(self):
|
||||
original = self._orig.raw._original_response
|
||||
|
||||
try:
|
||||
raw_version = self._orig.raw._original_response.version
|
||||
except AttributeError:
|
||||
# Assume HTTP/1.1
|
||||
raw_version = 11
|
||||
version = {
|
||||
9: '0.9',
|
||||
10: '1.0',
|
||||
11: '1.1',
|
||||
20: '2',
|
||||
}[original.version]
|
||||
}[raw_version]
|
||||
|
||||
status_line = 'HTTP/{version} {status} {reason}'.format(
|
||||
version=version,
|
||||
status=original.status,
|
||||
reason=original.reason
|
||||
)
|
||||
original = self._orig
|
||||
status_line = f'HTTP/{version} {original.status_code} {original.reason}'
|
||||
headers = [status_line]
|
||||
try:
|
||||
# `original.msg` is a `http.client.HTTPMessage` on Python 3
|
||||
# `_headers` is a 2-tuple
|
||||
headers.extend(
|
||||
'%s: %s' % header for header in original.msg._headers)
|
||||
except AttributeError:
|
||||
# and a `httplib.HTTPMessage` on Python 2.x
|
||||
# `headers` is a list of `name: val<CRLF>`.
|
||||
headers.extend(h.strip() for h in original.msg.headers)
|
||||
|
||||
headers.extend(
|
||||
': '.join(header)
|
||||
for header in original.headers.items()
|
||||
if header[0] != 'Set-Cookie'
|
||||
)
|
||||
headers.extend(
|
||||
f'Set-Cookie: {cookie}'
|
||||
for cookie in split_cookies(original.headers.get('Set-Cookie'))
|
||||
)
|
||||
return '\r\n'.join(headers)
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return self._orig.encoding or 'utf8'
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
# Only now the response body is fetched.
|
||||
# Shouldn't be touched unless the body is actually needed.
|
||||
return self._orig.content
|
||||
|
||||
|
||||
class HTTPRequest(HTTPMessage):
|
||||
"""A :class:`requests.models.Request` wrapper."""
|
||||
@ -105,7 +93,7 @@ class HTTPRequest(HTTPMessage):
|
||||
request_line = '{method} {path}{query} HTTP/1.1'.format(
|
||||
method=self._orig.method,
|
||||
path=url.path or '/',
|
||||
query='?' + url.query if url.query else ''
|
||||
query=f'?{url.query}' if url.query else ''
|
||||
)
|
||||
|
||||
headers = dict(self._orig.headers)
|
||||
@ -113,29 +101,18 @@ class HTTPRequest(HTTPMessage):
|
||||
headers['Host'] = url.netloc.split('@')[-1]
|
||||
|
||||
headers = [
|
||||
'%s: %s' % (
|
||||
name,
|
||||
value if isinstance(value, str) else value.decode('utf8')
|
||||
)
|
||||
f'{name}: {value if isinstance(value, str) else value.decode()}'
|
||||
for name, value in headers.items()
|
||||
]
|
||||
|
||||
headers.insert(0, request_line)
|
||||
headers = '\r\n'.join(headers).strip()
|
||||
|
||||
if isinstance(headers, bytes):
|
||||
# Python < 3
|
||||
headers = headers.decode('utf8')
|
||||
return headers
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return 'utf8'
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
body = self._orig.body
|
||||
if isinstance(body, str):
|
||||
# Happens with JSON/form request data parsed from the command line.
|
||||
body = body.encode('utf8')
|
||||
body = body.encode()
|
||||
return body or b''
|
||||
|
@ -1,19 +1,23 @@
|
||||
from __future__ import absolute_import
|
||||
import json
|
||||
from typing import Optional, Type
|
||||
|
||||
import pygments.lexer
|
||||
import pygments.token
|
||||
import pygments.styles
|
||||
import pygments.lexers
|
||||
import pygments.style
|
||||
import pygments.styles
|
||||
import pygments.token
|
||||
from pygments.formatters.terminal import TerminalFormatter
|
||||
from pygments.formatters.terminal256 import Terminal256Formatter
|
||||
from pygments.lexer import Lexer
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from pygments.lexers.special import TextLexer
|
||||
from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.plugins import FormatterPlugin
|
||||
from ..lexers.json import EnhancedJsonLexer
|
||||
from ...compat import is_windows
|
||||
from ...context import Environment
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
@ -24,7 +28,6 @@ if is_windows:
|
||||
# great and fruity seems to give the best result there.
|
||||
DEFAULT_STYLE = 'fruity'
|
||||
|
||||
|
||||
AVAILABLE_STYLES = set(pygments.styles.get_all_styles())
|
||||
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
|
||||
AVAILABLE_STYLES.add(AUTO_STYLE)
|
||||
@ -40,9 +43,14 @@ class ColorFormatter(FormatterPlugin):
|
||||
"""
|
||||
group_name = 'colors'
|
||||
|
||||
def __init__(self, env, explicit_json=False,
|
||||
color_scheme=DEFAULT_STYLE, **kwargs):
|
||||
super(ColorFormatter, self).__init__(**kwargs)
|
||||
def __init__(
|
||||
self,
|
||||
env: Environment,
|
||||
explicit_json=False,
|
||||
color_scheme=DEFAULT_STYLE,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if not env.colors:
|
||||
self.enabled = False
|
||||
@ -54,6 +62,7 @@ class ColorFormatter(FormatterPlugin):
|
||||
http_lexer = PygmentsHttpLexer()
|
||||
formatter = TerminalFormatter()
|
||||
else:
|
||||
from ..lexers.http import SimplifiedHTTPLexer
|
||||
http_lexer = SimplifiedHTTPLexer()
|
||||
formatter = Terminal256Formatter(
|
||||
style=self.get_style_class(color_scheme)
|
||||
@ -63,14 +72,14 @@ class ColorFormatter(FormatterPlugin):
|
||||
self.formatter = formatter
|
||||
self.http_lexer = http_lexer
|
||||
|
||||
def format_headers(self, headers):
|
||||
def format_headers(self, headers: str) -> str:
|
||||
return pygments.highlight(
|
||||
code=headers,
|
||||
lexer=self.http_lexer,
|
||||
formatter=self.formatter,
|
||||
).strip()
|
||||
|
||||
def format_body(self, body, mime):
|
||||
def format_body(self, body: str, mime: str) -> str:
|
||||
lexer = self.get_lexer_for_body(mime, body)
|
||||
if lexer:
|
||||
body = pygments.highlight(
|
||||
@ -78,24 +87,31 @@ class ColorFormatter(FormatterPlugin):
|
||||
lexer=lexer,
|
||||
formatter=self.formatter,
|
||||
)
|
||||
return body.strip()
|
||||
return body
|
||||
|
||||
def get_lexer_for_body(self, mime, body):
|
||||
def get_lexer_for_body(
|
||||
self, mime: str,
|
||||
body: str
|
||||
) -> Optional[Type[Lexer]]:
|
||||
return get_lexer(
|
||||
mime=mime,
|
||||
explicit_json=self.explicit_json,
|
||||
body=body,
|
||||
)
|
||||
|
||||
def get_style_class(self, color_scheme):
|
||||
@staticmethod
|
||||
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
|
||||
try:
|
||||
return pygments.styles.get_style_by_name(color_scheme)
|
||||
except ClassNotFound:
|
||||
return Solarized256Style
|
||||
|
||||
|
||||
def get_lexer(mime, explicit_json=False, body=''):
|
||||
|
||||
def get_lexer(
|
||||
mime: str,
|
||||
explicit_json=False,
|
||||
body=''
|
||||
) -> Optional[Type[Lexer]]:
|
||||
# Build candidate mime type and lexer names.
|
||||
mime_types, lexer_names = [mime], []
|
||||
type_, subtype = mime.split('/', 1)
|
||||
@ -105,8 +121,8 @@ def get_lexer(mime, explicit_json=False, body=''):
|
||||
subtype_name, subtype_suffix = subtype.split('+', 1)
|
||||
lexer_names.extend([subtype_name, subtype_suffix])
|
||||
mime_types.extend([
|
||||
'%s/%s' % (type_, subtype_name),
|
||||
'%s/%s' % (type_, subtype_suffix)
|
||||
f'{type_}/{subtype_name}',
|
||||
f'{type_}/{subtype_suffix}',
|
||||
])
|
||||
|
||||
# As a last resort, if no lexer feels responsible, and
|
||||
@ -138,57 +154,14 @@ def get_lexer(mime, explicit_json=False, body=''):
|
||||
else:
|
||||
lexer = pygments.lexers.get_lexer_by_name('json')
|
||||
|
||||
# Use our own JSON lexer: it supports JSON bodies preceded by non-JSON data
|
||||
# as well as legit JSON bodies.
|
||||
if isinstance(lexer, JsonLexer):
|
||||
lexer = EnhancedJsonLexer()
|
||||
|
||||
return lexer
|
||||
|
||||
|
||||
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
"""Simplified HTTP lexer for Pygments.
|
||||
|
||||
It only operates on headers and provides a stronger contrast between
|
||||
their names and values than the original one bundled with Pygments
|
||||
(:class:`pygments.lexers.text import HttpLexer`), especially when
|
||||
Solarized color scheme is used.
|
||||
|
||||
"""
|
||||
name = 'HTTP'
|
||||
aliases = ['http']
|
||||
filenames = ['*.http']
|
||||
tokens = {
|
||||
'root': [
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Name.Function,
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Namespace,
|
||||
pygments.token.Text,
|
||||
pygments.token.Keyword.Reserved,
|
||||
pygments.token.Operator,
|
||||
pygments.token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Keyword.Reserved, # 'HTTP'
|
||||
pygments.token.Operator, # '/'
|
||||
pygments.token.Number, # Version
|
||||
pygments.token.Text,
|
||||
pygments.token.Number, # Status code
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Exception, # Reason
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Attribute, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
pygments.token.String # Value
|
||||
))
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Solarized256Style(pygments.style.Style):
|
||||
"""
|
||||
solarized256
|
||||
|
@ -1,9 +1,13 @@
|
||||
from httpie.plugins import FormatterPlugin
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
|
||||
class HeadersFormatter(FormatterPlugin):
|
||||
|
||||
def format_headers(self, headers):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['headers']['sort']
|
||||
|
||||
def format_headers(self, headers: str) -> str:
|
||||
"""
|
||||
Sorts headers by name while retaining relative
|
||||
order of multiple headers with the same name.
|
||||
|
@ -1,15 +1,15 @@
|
||||
from __future__ import absolute_import
|
||||
import json
|
||||
|
||||
from httpie.plugins import FormatterPlugin
|
||||
|
||||
|
||||
DEFAULT_INDENT = 4
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
|
||||
class JSONFormatter(FormatterPlugin):
|
||||
|
||||
def format_body(self, body, mime):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['json']['format']
|
||||
|
||||
def format_body(self, body: str, mime: str) -> str:
|
||||
maybe_json = [
|
||||
'json',
|
||||
'javascript',
|
||||
@ -17,17 +17,18 @@ class JSONFormatter(FormatterPlugin):
|
||||
]
|
||||
if (self.kwargs['explicit_json']
|
||||
or any(token in mime for token in maybe_json)):
|
||||
from ..utils import load_prefixed_json
|
||||
try:
|
||||
obj = json.loads(body)
|
||||
data_prefix, json_obj = load_prefixed_json(body)
|
||||
except ValueError:
|
||||
pass # Invalid JSON, ignore.
|
||||
else:
|
||||
# Indent, sort keys by name, and avoid
|
||||
# unicode escapes to improve readability.
|
||||
body = json.dumps(
|
||||
obj=obj,
|
||||
sort_keys=True,
|
||||
body = data_prefix + json.dumps(
|
||||
obj=json_obj,
|
||||
sort_keys=self.format_options['json']['sort_keys'],
|
||||
ensure_ascii=False,
|
||||
indent=DEFAULT_INDENT
|
||||
indent=self.format_options['json']['indent']
|
||||
)
|
||||
return body
|
||||
|
59
httpie/output/formatters/xml.py
Normal file
59
httpie/output/formatters/xml.py
Normal file
@ -0,0 +1,59 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ...encoding import UTF8
|
||||
from ...plugins import FormatterPlugin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xml.dom.minidom import Document
|
||||
|
||||
|
||||
def parse_xml(data: str) -> 'Document':
|
||||
"""Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object."""
|
||||
from defusedxml.minidom import parseString
|
||||
return parseString(data)
|
||||
|
||||
|
||||
def pretty_xml(document: 'Document',
|
||||
encoding: Optional[str] = UTF8,
|
||||
indent: int = 2,
|
||||
standalone: Optional[bool] = None) -> str:
|
||||
"""Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string."""
|
||||
kwargs = {
|
||||
'encoding': encoding or UTF8,
|
||||
'indent': ' ' * indent,
|
||||
}
|
||||
if standalone is not None and sys.version_info >= (3, 9):
|
||||
kwargs['standalone'] = standalone
|
||||
body = document.toprettyxml(**kwargs).decode(kwargs['encoding'])
|
||||
|
||||
# Remove blank lines automatically added by `toprettyxml()`.
|
||||
return '\n'.join(line for line in body.splitlines() if line.strip())
|
||||
|
||||
|
||||
class XMLFormatter(FormatterPlugin):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.enabled = self.format_options['xml']['format']
|
||||
|
||||
def format_body(self, body: str, mime: str):
|
||||
if 'xml' not in mime:
|
||||
return body
|
||||
|
||||
from xml.parsers.expat import ExpatError
|
||||
from defusedxml.common import DefusedXmlException
|
||||
|
||||
try:
|
||||
parsed_body = parse_xml(body)
|
||||
except ExpatError:
|
||||
pass # Invalid XML, ignore.
|
||||
except DefusedXmlException:
|
||||
pass # Unsafe XML, ignore.
|
||||
else:
|
||||
body = pretty_xml(parsed_body,
|
||||
encoding=parsed_body.encoding,
|
||||
indent=self.format_options['xml']['indent'],
|
||||
standalone=parsed_body.standalone)
|
||||
|
||||
return body
|
0
httpie/output/lexers/__init__.py
Normal file
0
httpie/output/lexers/__init__.py
Normal file
49
httpie/output/lexers/http.py
Normal file
49
httpie/output/lexers/http.py
Normal file
@ -0,0 +1,49 @@
|
||||
import pygments
|
||||
|
||||
|
||||
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
|
||||
"""Simplified HTTP lexer for Pygments.
|
||||
|
||||
It only operates on headers and provides a stronger contrast between
|
||||
their names and values than the original one bundled with Pygments
|
||||
(:class:`pygments.lexers.text import HttpLexer`), especially when
|
||||
Solarized color scheme is used.
|
||||
|
||||
"""
|
||||
name = 'HTTP'
|
||||
aliases = ['http']
|
||||
filenames = ['*.http']
|
||||
tokens = {
|
||||
'root': [
|
||||
# Request-Line
|
||||
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Name.Function,
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Namespace,
|
||||
pygments.token.Text,
|
||||
pygments.token.Keyword.Reserved,
|
||||
pygments.token.Operator,
|
||||
pygments.token.Number
|
||||
)),
|
||||
# Response Status-Line
|
||||
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
|
||||
pygments.lexer.bygroups(
|
||||
pygments.token.Keyword.Reserved, # 'HTTP'
|
||||
pygments.token.Operator, # '/'
|
||||
pygments.token.Number, # Version
|
||||
pygments.token.Text,
|
||||
pygments.token.Number, # Status code
|
||||
pygments.token.Text,
|
||||
pygments.token.Name.Exception, # Reason
|
||||
)),
|
||||
# Header
|
||||
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
|
||||
pygments.token.Name.Attribute, # Name
|
||||
pygments.token.Text,
|
||||
pygments.token.Operator, # Colon
|
||||
pygments.token.Text,
|
||||
pygments.token.String # Value
|
||||
))
|
||||
]
|
||||
}
|
31
httpie/output/lexers/json.py
Normal file
31
httpie/output/lexers/json.py
Normal file
@ -0,0 +1,31 @@
|
||||
import re
|
||||
|
||||
from pygments.lexer import bygroups, using, RegexLexer
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from pygments.token import Token
|
||||
|
||||
PREFIX_TOKEN = Token.Error
|
||||
PREFIX_REGEX = r'[^{\["]+'
|
||||
|
||||
|
||||
class EnhancedJsonLexer(RegexLexer):
|
||||
"""
|
||||
Enhanced JSON lexer for Pygments.
|
||||
|
||||
It adds support for eventual data prefixing the actual JSON body.
|
||||
|
||||
"""
|
||||
name = 'JSON'
|
||||
flags = re.IGNORECASE | re.DOTALL
|
||||
tokens = {
|
||||
'root': [
|
||||
# Eventual non-JSON data prefix followed by actual JSON body.
|
||||
# FIX: data prefix + number (integer or float) are not correctly handled.
|
||||
(
|
||||
fr'({PREFIX_REGEX})' + r'((?:[{\["]|true|false|null).+)',
|
||||
bygroups(PREFIX_TOKEN, using(JsonLexer))
|
||||
),
|
||||
# JSON body.
|
||||
(r'.+', using(JsonLexer)),
|
||||
],
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import re
|
||||
from typing import Optional, List
|
||||
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.context import Environment
|
||||
from ..plugins import ConverterPlugin
|
||||
from ..plugins.registry import plugin_manager
|
||||
from ..context import Environment
|
||||
|
||||
|
||||
MIME_RE = re.compile(r'^[^/]+/[^/]+$')
|
||||
@ -11,19 +13,20 @@ def is_valid_mime(mime):
|
||||
return mime and MIME_RE.match(mime)
|
||||
|
||||
|
||||
class Conversion(object):
|
||||
class Conversion:
|
||||
|
||||
def get_converter(self, mime):
|
||||
@staticmethod
|
||||
def get_converter(mime: str) -> Optional[ConverterPlugin]:
|
||||
if is_valid_mime(mime):
|
||||
for converter_class in plugin_manager.get_converters():
|
||||
if converter_class.supports(mime):
|
||||
return converter_class(mime)
|
||||
|
||||
|
||||
class Formatting(object):
|
||||
class Formatting:
|
||||
"""A delegate class that invokes the actual processors."""
|
||||
|
||||
def __init__(self, groups, env=Environment(), **kwargs):
|
||||
def __init__(self, groups: List[str], env=Environment(), **kwargs):
|
||||
"""
|
||||
:param groups: names of processor groups to be applied
|
||||
:param env: Environment
|
||||
@ -38,12 +41,12 @@ class Formatting(object):
|
||||
if p.enabled:
|
||||
self.enabled_plugins.append(p)
|
||||
|
||||
def format_headers(self, headers):
|
||||
def format_headers(self, headers: str) -> str:
|
||||
for p in self.enabled_plugins:
|
||||
headers = p.format_headers(headers)
|
||||
return headers
|
||||
|
||||
def format_body(self, content, mime):
|
||||
def format_body(self, content: str, mime: str) -> str:
|
||||
if is_valid_mime(mime):
|
||||
for p in self.enabled_plugins:
|
||||
content = p.format_body(content, mime)
|
||||
|
@ -1,12 +1,11 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from itertools import chain
|
||||
from functools import partial
|
||||
from typing import Callable, Iterable, Union
|
||||
|
||||
from httpie.compat import str
|
||||
from httpie.context import Environment
|
||||
from httpie.models import HTTPRequest, HTTPResponse
|
||||
from httpie.input import (OUT_REQ_BODY, OUT_REQ_HEAD,
|
||||
OUT_RESP_HEAD, OUT_RESP_BODY)
|
||||
from httpie.output.processing import Formatting, Conversion
|
||||
from .processing import Conversion, Formatting
|
||||
from ..context import Environment
|
||||
from ..encoding import smart_decode, smart_encode, UTF8
|
||||
from ..models import HTTPMessage
|
||||
|
||||
|
||||
BINARY_SUPPRESSED_NOTICE = (
|
||||
@ -17,119 +16,26 @@ BINARY_SUPPRESSED_NOTICE = (
|
||||
)
|
||||
|
||||
|
||||
class BinarySuppressedError(Exception):
|
||||
class DataSuppressedError(Exception):
|
||||
message = None
|
||||
|
||||
|
||||
class BinarySuppressedError(DataSuppressedError):
|
||||
"""An error indicating that the body is binary and won't be written,
|
||||
e.g., for terminal output)."""
|
||||
|
||||
message = BINARY_SUPPRESSED_NOTICE
|
||||
|
||||
|
||||
def write_stream(stream, outfile, flush):
|
||||
"""Write the output stream."""
|
||||
try:
|
||||
# Writing bytes so we use the buffer interface (Python 3).
|
||||
buf = outfile.buffer
|
||||
except AttributeError:
|
||||
buf = outfile
|
||||
|
||||
for chunk in stream:
|
||||
buf.write(chunk)
|
||||
if flush:
|
||||
outfile.flush()
|
||||
|
||||
|
||||
def write_stream_with_colors_win_py3(stream, outfile, flush):
|
||||
"""Like `write`, but colorized chunks are written as text
|
||||
directly to `outfile` to ensure it gets processed by colorama.
|
||||
Applies only to Windows with Python 3 and colorized terminal output.
|
||||
|
||||
"""
|
||||
color = b'\x1b['
|
||||
encoding = outfile.encoding
|
||||
for chunk in stream:
|
||||
if color in chunk:
|
||||
outfile.write(chunk.decode(encoding))
|
||||
else:
|
||||
outfile.buffer.write(chunk)
|
||||
if flush:
|
||||
outfile.flush()
|
||||
|
||||
|
||||
def build_output_stream(args, env, request, response, output_options):
|
||||
"""Build and return a chain of iterators over the `request`-`response`
|
||||
exchange each of which yields `bytes` chunks.
|
||||
|
||||
"""
|
||||
req_h = OUT_REQ_HEAD in output_options
|
||||
req_b = OUT_REQ_BODY in output_options
|
||||
resp_h = OUT_RESP_HEAD in output_options
|
||||
resp_b = OUT_RESP_BODY in output_options
|
||||
req = req_h or req_b
|
||||
resp = resp_h or resp_b
|
||||
|
||||
output = []
|
||||
Stream = get_stream_type(env, args)
|
||||
|
||||
if req:
|
||||
output.append(Stream(
|
||||
msg=HTTPRequest(request),
|
||||
with_headers=req_h,
|
||||
with_body=req_b))
|
||||
|
||||
if req_b and resp:
|
||||
# Request/Response separator.
|
||||
output.append([b'\n\n'])
|
||||
|
||||
if resp:
|
||||
output.append(Stream(
|
||||
msg=HTTPResponse(response),
|
||||
with_headers=resp_h,
|
||||
with_body=resp_b))
|
||||
|
||||
if env.stdout_isatty and resp_b:
|
||||
# Ensure a blank line after the response body.
|
||||
# For terminal output only.
|
||||
output.append([b'\n\n'])
|
||||
|
||||
return chain(*output)
|
||||
|
||||
|
||||
def get_stream_type(env, args):
|
||||
"""Pick the right stream type based on `env` and `args`.
|
||||
Wrap it in a partial with the type-specific args so that
|
||||
we don't need to think what stream we are dealing with.
|
||||
|
||||
"""
|
||||
if not env.stdout_isatty and not args.prettify:
|
||||
Stream = partial(
|
||||
RawStream,
|
||||
chunk_size=RawStream.CHUNK_SIZE_BY_LINE
|
||||
if args.stream
|
||||
else RawStream.CHUNK_SIZE
|
||||
)
|
||||
elif args.prettify:
|
||||
Stream = partial(
|
||||
PrettyStream if args.stream else BufferedPrettyStream,
|
||||
env=env,
|
||||
conversion=Conversion(),
|
||||
formatting=Formatting(
|
||||
env=env,
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
),
|
||||
)
|
||||
else:
|
||||
Stream = partial(EncodedStream, env=env)
|
||||
|
||||
return Stream
|
||||
|
||||
|
||||
class BaseStream(object):
|
||||
class BaseStream(metaclass=ABCMeta):
|
||||
"""Base HTTP message output stream class."""
|
||||
|
||||
def __init__(self, msg, with_headers=True, with_body=True,
|
||||
on_body_chunk_downloaded=None):
|
||||
def __init__(
|
||||
self,
|
||||
msg: HTTPMessage,
|
||||
with_headers=True,
|
||||
with_body=True,
|
||||
on_body_chunk_downloaded: Callable[[bytes], None] = None
|
||||
):
|
||||
"""
|
||||
:param msg: a :class:`models.HTTPMessage` subclass
|
||||
:param with_headers: if `True`, headers will be included
|
||||
@ -142,15 +48,15 @@ class BaseStream(object):
|
||||
self.with_body = with_body
|
||||
self.on_body_chunk_downloaded = on_body_chunk_downloaded
|
||||
|
||||
def get_headers(self):
|
||||
def get_headers(self) -> bytes:
|
||||
"""Return the headers' bytes."""
|
||||
return self.msg.headers.encode('utf8')
|
||||
return self.msg.headers.encode()
|
||||
|
||||
def iter_body(self):
|
||||
@abstractmethod
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
"""Return an iterator over the message body."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterable[bytes]:
|
||||
"""Return an iterator over `self.msg`."""
|
||||
if self.with_headers:
|
||||
yield self.get_headers()
|
||||
@ -162,7 +68,7 @@ class BaseStream(object):
|
||||
yield chunk
|
||||
if self.on_body_chunk_downloaded:
|
||||
self.on_body_chunk_downloaded(chunk)
|
||||
except BinarySuppressedError as e:
|
||||
except DataSuppressedError as e:
|
||||
if self.with_headers:
|
||||
yield b'\n'
|
||||
yield e.message
|
||||
@ -175,10 +81,10 @@ class RawStream(BaseStream):
|
||||
CHUNK_SIZE_BY_LINE = 1
|
||||
|
||||
def __init__(self, chunk_size=CHUNK_SIZE, **kwargs):
|
||||
super(RawStream, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.chunk_size = chunk_size
|
||||
|
||||
def iter_body(self):
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
return self.msg.iter_body(self.chunk_size)
|
||||
|
||||
|
||||
@ -192,29 +98,31 @@ class EncodedStream(BaseStream):
|
||||
"""
|
||||
CHUNK_SIZE = 1
|
||||
|
||||
def __init__(self, env=Environment(), **kwargs):
|
||||
|
||||
super(EncodedStream, self).__init__(**kwargs)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env=Environment(),
|
||||
mime_overwrite: str = None,
|
||||
encoding_overwrite: str = None,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.mime = mime_overwrite or self.msg.content_type
|
||||
self.encoding = encoding_overwrite or self.msg.encoding
|
||||
if env.stdout_isatty:
|
||||
# Use the encoding supported by the terminal.
|
||||
output_encoding = env.stdout_encoding
|
||||
else:
|
||||
# Preserve the message encoding.
|
||||
output_encoding = self.msg.encoding
|
||||
# Default to UTF-8 when unsure.
|
||||
self.output_encoding = output_encoding or UTF8
|
||||
|
||||
# Default to utf8 when unsure.
|
||||
self.output_encoding = output_encoding or 'utf8'
|
||||
|
||||
def iter_body(self):
|
||||
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
|
||||
|
||||
if b'\0' in line:
|
||||
raise BinarySuppressedError()
|
||||
|
||||
yield line.decode(self.msg.encoding) \
|
||||
.encode(self.output_encoding, 'replace') + lf
|
||||
line = smart_decode(line, self.encoding)
|
||||
yield smart_encode(line, self.output_encoding) + lf
|
||||
|
||||
|
||||
class PrettyStream(EncodedStream):
|
||||
@ -228,17 +136,20 @@ class PrettyStream(EncodedStream):
|
||||
|
||||
CHUNK_SIZE = 1
|
||||
|
||||
def __init__(self, conversion, formatting, **kwargs):
|
||||
super(PrettyStream, self).__init__(**kwargs)
|
||||
def __init__(
|
||||
self, conversion: Conversion,
|
||||
formatting: Formatting,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.formatting = formatting
|
||||
self.conversion = conversion
|
||||
self.mime = self.msg.content_type.split(';')[0]
|
||||
|
||||
def get_headers(self):
|
||||
def get_headers(self) -> bytes:
|
||||
return self.formatting.format_headers(
|
||||
self.msg.headers).encode(self.output_encoding)
|
||||
|
||||
def iter_body(self):
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
first_chunk = True
|
||||
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
|
||||
for line, lf in iter_lines:
|
||||
@ -259,13 +170,13 @@ class PrettyStream(EncodedStream):
|
||||
yield self.process_body(line) + lf
|
||||
first_chunk = False
|
||||
|
||||
def process_body(self, chunk):
|
||||
def process_body(self, chunk: Union[str, bytes]) -> bytes:
|
||||
if not isinstance(chunk, str):
|
||||
# Text when a converter has been used,
|
||||
# otherwise it will always be bytes.
|
||||
chunk = chunk.decode(self.msg.encoding, 'replace')
|
||||
chunk = smart_decode(chunk, self.encoding)
|
||||
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
|
||||
return chunk.encode(self.output_encoding, 'replace')
|
||||
return smart_encode(chunk, self.output_encoding)
|
||||
|
||||
|
||||
class BufferedPrettyStream(PrettyStream):
|
||||
@ -278,7 +189,7 @@ class BufferedPrettyStream(PrettyStream):
|
||||
|
||||
CHUNK_SIZE = 1024 * 10
|
||||
|
||||
def iter_body(self):
|
||||
def iter_body(self) -> Iterable[bytes]:
|
||||
# Read the whole body before prettifying it,
|
||||
# but bail out immediately if the body is binary.
|
||||
converter = None
|
||||
|
37
httpie/output/utils.py
Normal file
37
httpie/output/utils.py
Normal file
@ -0,0 +1,37 @@
|
||||
import json
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
from ..utils import load_json_preserve_order_and_dupe_keys
|
||||
from .lexers.json import PREFIX_REGEX
|
||||
|
||||
|
||||
def load_prefixed_json(data: str) -> Tuple[str, json.JSONDecoder]:
|
||||
"""Simple JSON loading from `data`.
|
||||
|
||||
"""
|
||||
# First, the full data.
|
||||
try:
|
||||
return '', load_json_preserve_order_and_dupe_keys(data)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Then, try to find the start of the actual body.
|
||||
data_prefix, body = parse_prefixed_json(data)
|
||||
try:
|
||||
return data_prefix, load_json_preserve_order_and_dupe_keys(body)
|
||||
except ValueError:
|
||||
raise ValueError('Invalid JSON')
|
||||
|
||||
|
||||
def parse_prefixed_json(data: str) -> Tuple[str, str]:
|
||||
"""Find the potential JSON body from `data`.
|
||||
|
||||
Sometimes the JSON body is prefixed with a XSSI magic string, specific to the server.
|
||||
Return a tuple (data prefix, actual JSON body).
|
||||
|
||||
"""
|
||||
matches = re.findall(PREFIX_REGEX, data)
|
||||
data_prefix = matches[0] if matches else ''
|
||||
body = data[len(data_prefix):]
|
||||
return data_prefix, body
|
162
httpie/output/writer.py
Normal file
162
httpie/output/writer.py
Normal file
@ -0,0 +1,162 @@
|
||||
import argparse
|
||||
import errno
|
||||
from typing import IO, TextIO, Tuple, Type, Union
|
||||
|
||||
import requests
|
||||
|
||||
from ..context import Environment
|
||||
from ..models import HTTPRequest, HTTPResponse, HTTPMessage
|
||||
from .processing import Conversion, Formatting
|
||||
from .streams import (
|
||||
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
|
||||
)
|
||||
|
||||
|
||||
MESSAGE_SEPARATOR = '\n\n'
|
||||
MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
|
||||
|
||||
|
||||
def write_message(
|
||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
||||
env: Environment,
|
||||
args: argparse.Namespace,
|
||||
with_headers=False,
|
||||
with_body=False,
|
||||
):
|
||||
if not (with_body or with_headers):
|
||||
return
|
||||
write_stream_kwargs = {
|
||||
'stream': build_output_stream_for_message(
|
||||
args=args,
|
||||
env=env,
|
||||
requests_message=requests_message,
|
||||
with_body=with_body,
|
||||
with_headers=with_headers,
|
||||
),
|
||||
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
|
||||
'outfile': env.stdout,
|
||||
'flush': env.stdout_isatty or args.stream
|
||||
}
|
||||
try:
|
||||
if env.is_windows and 'colors' in args.prettify:
|
||||
write_stream_with_colors_win(**write_stream_kwargs)
|
||||
else:
|
||||
write_stream(**write_stream_kwargs)
|
||||
except OSError as e:
|
||||
show_traceback = args.debug or args.traceback
|
||||
if not show_traceback and e.errno == errno.EPIPE:
|
||||
# Ignore broken pipes unless --traceback.
|
||||
env.stderr.write('\n')
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def write_stream(
|
||||
stream: BaseStream,
|
||||
outfile: Union[IO, TextIO],
|
||||
flush: bool
|
||||
):
|
||||
"""Write the output stream."""
|
||||
try:
|
||||
# Writing bytes so we use the buffer interface.
|
||||
buf = outfile.buffer
|
||||
except AttributeError:
|
||||
buf = outfile
|
||||
|
||||
for chunk in stream:
|
||||
buf.write(chunk)
|
||||
if flush:
|
||||
outfile.flush()
|
||||
|
||||
|
||||
def write_stream_with_colors_win(
|
||||
stream: 'BaseStream',
|
||||
outfile: TextIO,
|
||||
flush: bool
|
||||
):
|
||||
"""Like `write`, but colorized chunks are written as text
|
||||
directly to `outfile` to ensure it gets processed by colorama.
|
||||
Applies only to Windows and colorized terminal output.
|
||||
|
||||
"""
|
||||
color = b'\x1b['
|
||||
encoding = outfile.encoding
|
||||
for chunk in stream:
|
||||
if color in chunk:
|
||||
outfile.write(chunk.decode(encoding))
|
||||
else:
|
||||
outfile.buffer.write(chunk)
|
||||
if flush:
|
||||
outfile.flush()
|
||||
|
||||
|
||||
def build_output_stream_for_message(
|
||||
args: argparse.Namespace,
|
||||
env: Environment,
|
||||
requests_message: Union[requests.PreparedRequest, requests.Response],
|
||||
with_headers: bool,
|
||||
with_body: bool,
|
||||
):
|
||||
message_type = {
|
||||
requests.PreparedRequest: HTTPRequest,
|
||||
requests.Response: HTTPResponse,
|
||||
}[type(requests_message)]
|
||||
stream_class, stream_kwargs = get_stream_type_and_kwargs(
|
||||
env=env,
|
||||
args=args,
|
||||
message_type=message_type,
|
||||
)
|
||||
yield from stream_class(
|
||||
msg=message_type(requests_message),
|
||||
with_headers=with_headers,
|
||||
with_body=with_body,
|
||||
**stream_kwargs,
|
||||
)
|
||||
if (env.stdout_isatty and with_body
|
||||
and not getattr(requests_message, 'is_body_upload_chunk', False)):
|
||||
# Ensure a blank line after the response body.
|
||||
# For terminal output only.
|
||||
yield MESSAGE_SEPARATOR_BYTES
|
||||
|
||||
|
||||
def get_stream_type_and_kwargs(
|
||||
env: Environment,
|
||||
args: argparse.Namespace,
|
||||
message_type: Type[HTTPMessage],
|
||||
) -> Tuple[Type['BaseStream'], dict]:
|
||||
"""Pick the right stream type and kwargs for it based on `env` and `args`.
|
||||
|
||||
"""
|
||||
if not env.stdout_isatty and not args.prettify:
|
||||
stream_class = RawStream
|
||||
stream_kwargs = {
|
||||
'chunk_size': (
|
||||
RawStream.CHUNK_SIZE_BY_LINE
|
||||
if args.stream
|
||||
else RawStream.CHUNK_SIZE
|
||||
)
|
||||
}
|
||||
else:
|
||||
stream_class = EncodedStream
|
||||
stream_kwargs = {
|
||||
'env': env,
|
||||
}
|
||||
if message_type is HTTPResponse:
|
||||
stream_kwargs.update({
|
||||
'mime_overwrite': args.response_mime,
|
||||
'encoding_overwrite': args.response_charset,
|
||||
})
|
||||
if args.prettify:
|
||||
stream_class = PrettyStream if args.stream else BufferedPrettyStream
|
||||
stream_kwargs.update({
|
||||
'conversion': Conversion(),
|
||||
'formatting': Formatting(
|
||||
env=env,
|
||||
groups=args.prettify,
|
||||
color_scheme=args.style,
|
||||
explicit_json=args.json,
|
||||
format_options=args.format_options,
|
||||
)
|
||||
})
|
||||
|
||||
return stream_class, stream_kwargs
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user