Compare commits

...

184 Commits
2.5.0 ... 3.0.0

Author SHA1 Message Date
88140422a9 3.0 release prep (#1272) 2022-01-21 20:34:38 +03:00
d2d40eb336 Finish docs for v3.0.0 (#1269)
* WIP

* Rewrite the introduction segment of the Nested JSON

Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>
2022-01-21 20:24:07 +03:00
cd877a5e08 Remove 3.6 support / discontinue less available platforms (#1267)
* Remove redundant systems

* Drop it from the docs

* Remove the packaging info about the legacy systems

* Fix some typos

* Drop support for python 3.6
2022-01-14 08:49:05 -08:00
87629706c9 Change the default style for windows from fruity to auto (#1268) 2022-01-14 08:47:10 -08:00
3856f94d3d Update the brew file 2022-01-14 13:27:19 +03:00
dc30919893 use constants 2022-01-13 19:54:43 +03:00
fb82f44cd1 Use enums 2022-01-13 19:54:43 +03:00
eb4e32ca28 A few edits 2022-01-13 19:54:43 +03:00
980bd59e29 Rewrite the docs 2022-01-13 19:54:43 +03:00
2cda966384 Implement escaped integers 2022-01-13 19:54:43 +03:00
7bf373751d Implement HTTPie Nested JSON v2 2022-01-13 19:54:43 +03:00
21faddc4b9 Proper separation of meta/body 2022-01-13 15:04:44 +03:00
c126bc11c7 Make the stdin wait tests more reliable 2022-01-13 15:04:30 +03:00
00c859c51d Add warnings when there is no incoming data from stdin (#1256)
* Add warnings when there is no incoming data from stdin

* Pass os.environ as well

* Apply suggestions
2022-01-12 06:07:34 -08:00
508788ca56 Fix two typos in docs/README.md (#1261)
contaning  -> containing
overwriten -> overwritten

Co-authored-by: greg <gmyers@gitlab.com>
2022-01-10 02:48:42 -08:00
4c56d894ba Fix --raw with --chunked (#1254)
* Fix --raw with --chunked

* Better naming / annotations

* More annotations
2021-12-29 12:41:44 +03:00
0e10e23dca Mention explicitly about prompted passwords are stored as raw in the docs 2021-12-29 12:03:44 +03:00
06512c72a3 Include the original issue in the changelog 2021-12-29 12:02:24 +03:00
8d84248ee3 Add the changelog entry 2021-12-29 12:01:49 +03:00
17ed3bb8c5 Store prompted passwords in local sessions (#1239)
Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>
2021-12-29 12:00:47 +03:00
05c02f0f39 Update shortcuts as well 2021-12-24 11:53:31 +03:00
0ebc9a7e09 Mention about levels in -v 2021-12-24 11:53:15 +03:00
c692669526 Fix -v docs to include BASE_OUTPUT_OPTIONS 2021-12-24 11:51:11 +03:00
747accc2ae Include response metadata in --print help 2021-12-24 11:50:19 +03:00
f3b500119c Implement basic metrics layout & total elapsed time (#1250)
* Initial metadata processing

* Dynamic coloring and other stuff

* Use -vv / --meta

* More testing

* Cleanup

* Tweek message

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-23 12:13:25 -08:00
e0e03f3237 Better DNS error handling (#1249)
* Better DNS error handling

* Update httpie/core.py

Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-23 11:35:30 -08:00
be87da8bbd Formalize @ suffix for all operators (#1225)
* Formalize @ suffix for all operators

* Separate the section

* Address suggestions
2021-12-23 11:06:35 -08:00
e09401b81a Optimize encoding detection (#1243)
* Optimize encoding detection

* Use a threshold based system
2021-12-23 11:05:58 -08:00
5a83a9ebc4 Test https as well 2021-12-21 20:33:23 +03:00
c97ec93a19 Test httpie 2021-12-21 20:33:09 +03:00
2d15659b16 Make brew action triggerable 2021-12-21 20:28:42 +03:00
021b41c9e5 Make snap action triggerable 2021-12-21 20:28:23 +03:00
8dc6c0df77 Implement new pie and pie-light styles (#1238)
* Implement new `pie` and `pie-light` styles

* Change some pallete

* Integrate the color palette

* some docs

* some docs

* Rework on code generation

* Apply suggestions from code review

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-19 02:41:42 -08:00
1bd8422fb5 Improve startup time when pyOpenSSL is available on the environment (#1233) 2021-12-17 00:00:22 -08:00
c237e15108 Faster downloads through bigger chunks / less buffering (#1236) 2021-12-17 00:00:03 -08:00
a5d8b51e47 Implement httpie upgrade for upgrading plugins (#1241)
* Implement `httpie upgrade` for upgrading plugins

* Support upgrades for every installation type

* Fix decoding problems
2021-12-16 23:59:39 -08:00
2b78d04410 Strip out extra variables from the actual mime type (#1244)
* Strip out extra variables from the actual mime type

* mention in changelog

* Update CHANGELOG.md

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-16 07:04:34 -08:00
7bd7aa20d2 (stale action) bump operations per run to 300 2021-12-16 12:24:52 +03:00
7ae44aefe2 (stale action) get rid of stale message, only comment on closing 2021-12-16 12:19:25 +03:00
28e874535a (stale action) bump days to 30 2021-12-16 12:18:09 +03:00
340fef6278 (stale action) Fix typo in closing message 2021-12-16 12:17:55 +03:00
088b6cdb0c Move stale action from debug to actual run 2021-12-16 12:14:50 +03:00
43462f8af0 Only configure with workflow_dispatch 2021-12-16 12:11:12 +03:00
e4b2751a52 Set stale action to run on workflow dispatch 2021-12-16 12:09:31 +03:00
f94c12d8ca Close all stale PRs (#1245) 2021-12-16 12:06:00 +03:00
3db1cdba4c Don't inconsistently add XML declarations (#1227) 2021-12-14 07:15:19 -08:00
4f7f59b990 Add initial benchmarking infrastructure (#1232)
* Add initial benchmarking infrastructure

* Add CI file

* Try to comment on commits

* Implement file download benchmarks!

* drop commit comments (they dont work)

* Allow running local binary

* Better action

* More docs!

* Better look?

* even better look

* add pretty=all, none benchmarks
2021-12-14 07:05:25 -08:00
e30ec6be42 Remove unnecessary empty line in CHANGELOG 2021-12-09 12:46:19 +03:00
207b970d94 Automatically enable --stream on server sent events (#1226)
* Automatically enable --stream when used chunked encoding

* try fix 3.6 mock issue

* Only enable on text/event-stream

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-08 07:49:12 -08:00
62e43abc86 Ignore crashes that happen on the 3rd party plugins (#1228)
* Ignore crashes that happen on the 3rd party plugins

* Give a suggestion about how to uninstall
2021-12-08 07:45:07 -08:00
ea8e22677a Fix snapcraft packaging (#1235) 2021-12-08 01:20:58 -08:00
df58ec683e Add nested JSON syntax to the HTTPie DSL (#1224)
* Add support for nested JSON syntax (#1169)

Co-authored-by: Batuhan Taskaya <isidentical@gmail.com>
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* minor improvements

* unpack top level lists

* Write more docs

* doc style changes

* fix double quotes

Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-03 02:17:45 -08:00
8fe1f08a37 Changelog 2021-12-01 20:51:00 +01:00
521ddde4c5 CHANGELOG.md 2021-12-01 20:49:03 +01:00
3457806df1 CHANGELOG.md 2021-12-01 20:45:54 +01:00
840f77d2a8 Tweak changelog & 3.0.0.dev0 2021-12-01 20:44:04 +01:00
6522ce06d0 Add plugin management changelog entry (#1223)
* Add plugin management changelog entry

* Update CHANGELOG.md

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-01 10:20:16 -08:00
f927065416 brew: add multidict (#1222) 2021-12-01 10:19:38 -08:00
151becec2b Improve startup time with lazy loading some args (#1221)
* Improve startup time with lazy loading some args

* add some tests

* Add changelog entry

* Update CHANGELOG.md

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-12-01 10:15:59 -08:00
ba8e4097e8 Support ==@ syntax for query parameter values from file (#1218)
Co-authored-by: Vladimir Berkutov <vladimir.berkutov@gmail.com>

Co-authored-by: Vladimir Berkutov <vladimir.berkutov@gmail.com>
2021-12-01 10:09:39 -08:00
00b366a81f Implement Bearer Auth (#1216) 2021-12-01 09:37:57 -08:00
5bf696d113 Fix packit CI (#1219) 2021-11-30 13:49:38 +03:00
3081fc1a3c Add httpie --version (#1220) 2021-11-30 13:18:37 +03:00
245cede2c2 cmd: Implement httpie plugins interface (#1200) 2021-11-30 11:12:51 +03:00
6bdcdf1eba Proper JSON handling for :=/:=@ (#1213)
* Proper JSON handling for :=/:=@

* document the behavior

* fixup docs
2021-11-26 03:45:46 -08:00
0fc6331ee0 Change PyPi to PyPI (#1203)
* Change `PyPi` to `PyPI`

* fix: change `PyPi` to `PyPI` in method yaml file
2021-11-25 14:06:34 -08:00
ef62fc11bf core: support custom request/response classes (#1205)
* core: support custom request/response classes

* Move to `httpie.models`, prefix with `Requests`
2021-11-24 15:45:39 -08:00
c000886546 Preserve individual headers with the same name on responses (#1208)
* Preserve individual headers with the same name on responses

* Rename RequestHeadersDict to HTTPHeadersDict

* Update tests/utils/http_server.py

* Update tests/utils/http_server.py

* Update httpie/adapters.py

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-11-24 15:41:37 -08:00
cfcd7413d1 Fix README broken links to old locations (#1209) 2021-11-21 02:38:05 -08:00
7dfa001d2c Consistent userdir/name example (#1210) 2021-11-21 02:32:00 -08:00
06d9c14e7a Add $ http :// error handling test 2021-11-05 14:11:30 +01:00
861b8b36a8 Strip leading :// from URLs to allow quick conversion of a pasted URL to calls (#1197)
* Strip leading `://` from URLs to allow quick conversion of a pasted URL to calls

Closes #1195

* Markdown lint

* Cleanup

* Cleanup

* Drop extraneous space

* Fix example
2021-11-05 13:59:23 +01:00
434512e92f Update bug_report.md 2021-11-04 23:20:46 +01:00
72735d9d59 Update config.json 2021-11-03 12:50:07 +01:00
7cdd74fece Support multiple headers sharing the same name (#1190)
* Support multiple headers sharing the same name

* Apply suggestions

* Don't normalize HTTP header names

* apply visual suggestions

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* bump down multidict to 4.7.0

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-10-31 15:04:39 +01:00
d40f06687f Update README.md 2021-10-29 11:33:46 +02:00
0d9c8b88b3 Change Chocolatey owner 2021-10-25 17:18:53 +02:00
cff45276b5 Fix Snap autocompletion (#1189) 2021-10-25 16:36:34 +02:00
e75e0a0565 Change Void Linux maintainer 2021-10-25 16:25:59 +02:00
19e48ba901 Update Spack metadata 2021-10-25 16:19:49 +02:00
a9b8513f62 Update Gentoo metadata 2021-10-25 16:16:26 +02:00
7985cf60c8 Fix Gentoo example link 2021-10-25 16:15:27 +02:00
5dc4a26277 Remove myself from the HTTPie team 2021-10-25 14:55:45 +02:00
7775422afb Add contributors list update to the release process 2021-10-25 14:54:59 +02:00
2be43e698a Add HTTPie 2.6.0 blog post link
https://httpie.io/blog/httpie-2.6.0
2021-10-24 19:44:02 +02:00
3abc76f6d5 Tiny docstring clean-up 2021-10-19 10:24:01 +02:00
021eb651e0 Bump the version to 2.7.0.dev0 (#1188) 2021-10-19 10:21:45 +02:00
419427cfb6 Update downstream files for HTTPie 2.6.0 (#1186)
* Update Alpine package

* Add charset-normalizer deps for Alpine

It currently does not exist. We will need to add it ourselves.

* Update Gentoo package

* Update Brew formula

* Update MacPorts port

* Fix Gentoo deps

* Update examples

* Update Void Linux package

* Update Void Linux commands

* Update Chocolateur package

* Review DEbian packaging details

* Simplify Void Linux package

* Update more packages

* Update summary everywhere

* Remove temporary file

* Update Chocolatey package URL

* Updates

* Update Spack
2021-10-19 10:18:35 +02:00
7500912be1 Corrected command for installing development version on Windows (#1187) 2021-10-15 18:01:07 +02:00
1b4048aefc dnf/yum update is the same as dnf upgrade -- it updates all packages (#1184)
No reason to run it before installing or upgrading httpie.
This is not apt.
2021-10-15 15:29:06 +02:00
7885f5cd66 Minor version changes in the Fedora packaging docs (#1185) 2021-10-15 15:24:21 +02:00
3e414d731c Update the awesome contributors list to HTTPie 2.6.0 2021-10-14 17:17:14 +02:00
d8f6a5fe52 Blank master_and_released_docs_differ_after 2021-10-14 11:30:13 +02:00
cee283a01a Update setup.py 2021-10-14 11:27:12 +02:00
5c267003c7 Update links 2021-10-14 11:25:13 +02:00
cdab8e67cb Release workflow: fix 2021-10-14 10:56:13 +02:00
6c6093a46d Configure PyPi for the release workflow 2021-10-14 10:45:31 +02:00
42af2f786f v2.6.0 (#1182)
[skip ci]
2021-10-14 10:36:39 +02:00
a65771e271 Add a script that lists all contributors to a release (#1181)
* Add a script that lists all contributors to a release

We will keep a contributors database (simple JSON file) where
each entry is a contributor (either a committer, either an issue reporter,
either both) with some nicknames (GitHub, and Twitter).
The file will be used to craft credits on our release blog posts and to ping
them on Twitter.

* Add templates

* Missing docstring

* Clean-up

* Tweak
2021-10-14 10:33:14 +02:00
7b683d4b57 Update CHANGELOG.md 2021-10-13 23:37:40 +02:00
a15fd6f966 Add --response=mime and --response=charset docs (#1179)
* Add the "display encoding" section in the docs

* Remove repetition

* `--response=mime` / `--response=charset` docs

* Cleanup

* Cleanup

* Cleanup

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-10-13 23:32:46 +02:00
19691bba68 Packaging documentation tweaks 2021-10-11 17:42:29 +02:00
344491ba8e Tweak the Chocolatey package installation file 2021-10-11 10:07:24 +02:00
9f6fa090df Auto-update install docs
Via .github/workflows/docs-update-install.yml
2021-10-10 18:58:57 +00:00
59f4ef03cc Remove macOS/Snap
snapd is not available on macOS yet
2021-10-10 20:58:05 +02:00
ef92e2a74a Auto-update install docs
Via .github/workflows/docs-update-install.yml
2021-10-10 18:46:46 +00:00
1171984ec2 Better links for snap on macos 2021-10-10 20:45:53 +02:00
ce9746b1f8 Auto-update install docs
Via .github/workflows/docs-update-install.yml
2021-10-10 18:25:59 +00:00
6b99e1c932 Link GitHub action file in generated commit 2021-10-10 20:25:12 +02:00
7d418aecd0 Auto-update installation instructions in the docs 2021-10-10 18:18:39 +00:00
459cdfcf53 Tweak install docs template
Shorten setup, add missing comma
2021-10-10 20:17:49 +02:00
ab8512f96c Add --compress documentation (#1173)
* Add --compress documentation

* Apply suggestions from code review

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* Update docs/README.md

* Update docs/README.md

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-10-08 18:38:40 +02:00
6befaf9067 Added the ability to silence warnings via double -q or --quiet (#1175)
* change behavior of '--quiet' to silence errors and warnings when passed twice together with '--check-status'

* Apply suggestions from code review

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* remove header, trailing comma, rename constant and variable

* fix flags for tests

* [skip ci] Update ticket number

Co-authored-by: Dave <d.kreeft@outlook.com>
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
Co-authored-by: Mickaël Schoentgen <contact@tiger-222.fr>
2021-10-08 14:18:11 +02:00
1b7f74c2b2 Add a workflow to control Snap publications (#1176) 2021-10-08 11:24:24 +02:00
50f57f8c82 Add help output for --chunked (#1174) 2021-10-07 17:37:06 +02:00
555afef486 Update README.md 2021-10-07 16:21:36 +02:00
476eb4f0d9 Use HTTPie repository everywhere 2021-10-07 16:15:52 +02:00
3869c3ce99 Remove unused import 2021-10-07 14:41:03 +02:00
17f74f10f3 Add summary to Chocolatey metadata
It is strongly recommended to add a summary.
It will be effective for the next release though.
2021-10-07 14:34:33 +02:00
1e094d0a79 Fix Snapcraft and Spack anchors 2021-10-07 14:06:06 +02:00
bd227c0364 Specify the API key to submit Chocolatey packages 2021-10-07 13:58:02 +02:00
9dda23a322 Add Chocolatey packaging information (#1172)
* Add Chocolatey packaging information

* Fix working directory

* Fix Python dependency

* Update icon URL

* Fix local installation

* Simplify links

* Remove the workflow, it adds no real value and th etime to fix is is not worth
2021-10-07 13:53:11 +02:00
ef4fa20ceb Tweak 2021-10-06 19:31:43 +02:00
7e0bed4e54 Add more types and docs for plugins API II. 2021-10-06 19:28:21 +02:00
e1627803fe Add more types and docs for plugins API 2021-10-06 19:24:10 +02:00
f954c9e2b7 Update CHANGELOG.md 2021-10-06 17:35:07 +02:00
80e83f0463 Ignore more venv folders and VS Code folder 2021-10-06 17:29:03 +02:00
4f1c9441c5 Fix encoding error with non-prettified encoded responses (#1168)
* Fix encoding error with non-prettified encoded responses

Removed `--format-option response.as` an promote `--response-as`: using
the format option would be misleading as it is now also used by non-prettified
responses.

* Encoding refactoring

* split --response-as into --response-mime and --response-charset
* add support for Content-Type charset for requests printed to terminal
* add support charset detection for requests printed to terminal without a Content-Type charset
* etc.

* `test_unicode.py` → `test_encoding.py`

* Drop sequence length check

* Clean-up tests

* [skip ci] Tweaks

* Use the compatible release clause for `charset_normalizer` requirement

Cf. https://www.python.org/dev/peps/pep-0440/#version-specifiers

* Clean-up

* Partially revert d52a4833e4

* Changelog

* Tweak tests

* [skip ci] Better test name

* Cleanup tests and add request body charset detection

* More test suite cleanups

* Cleanup

* Fix code style in test

* Improve detect_encoding() docstring

* Uniformize pytest.mark.parametrize() calls

* [skip ci] Comment out TODOs (will be tackled in a specific PR)

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-10-06 17:27:07 +02:00
7989e438d2 Add documentation about our release process (#1159)
* Add documentation about our release process

* Fixes

* Add company-related tasks, enable back WIP pages

* Fix WIP links

* Add AOSC OS

* Add WIP for AOSC OS

* Tweak

* Remove maintainers email IDs

* Use GH nicknames

* Remove useless WIP for brew

* Tweaks
2021-10-06 16:45:44 +02:00
93114072c8 Fix looked path for workflow testing packages 2021-10-06 11:21:54 +02:00
08751d3672 Add install/update instructions database (#1160)
* Add install/update instructions database

* Update the database

* Revert README changes

They will be overwritten later.

* Revert

* Tweak

* Tweaks

* Upgrade database

* Complete commands

Still not sure about Spack upgrades.

* Sort

* Doc generation script draft

* Remove OS names from tool names

* Fix Linuxbrew name

* `wheel` already installs `setuptools`

* Gen docs

* Update

* Tweak

* Add a GitHub workflow to check for outdated installation instructions

* Fix return value

* Test

* Delete test

* Rename the script

* Add `make doc-install-inst`

* Add missing dev requirement

* The first tool is the primary we want to display

Then they are simply sorted by `tool.title`.

* Sort OSes by name

* Refactoring, jinja template, etc.

* Add tool title uniqueness `assert`, fix platform list extra `\n`

* Rebuild docs

* Update generate.py

* Update README.md

* Update methods.yml

* Update distros derived, more assertions

* Tweaks

* Add workflow to auto-update the docs

* Do not hide the command

* Tweaks

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-10-06 11:18:27 +02:00
0c9d701618 Add make install-reqs to install packages without creating env 2021-10-05 21:37:48 +02:00
a3fa016428 Cover on Python 3.10 2021-10-05 21:30:28 +02:00
9c52449344 Add self to paths; same paths for PR and push 2021-10-05 21:28:39 +02:00
e4e4927567 Test on Python 3.10 2021-10-05 21:25:10 +02:00
031b4b89e3 Fix docs formatting 2021-10-05 15:46:10 +02:00
e1c08a3de5 Mention Snapcraft for unstable version installation 2021-10-05 15:45:34 +02:00
033798adc1 Update README.md 2021-10-03 03:20:23 +02:00
6a4e985f71 Remove redundant/inconsistent article 2021-10-03 03:08:05 +02:00
a6c70334cf Expand HTTP method docs
* mention `POST` without a body
* mention `GET` with a body
* mention custom method names
* include examples for default `GET`/`POST`
2021-10-03 03:05:37 +02:00
7388401134 Update setup.py 2021-10-02 16:50:39 +02:00
4ef31ecf71 Update utils.py 2021-10-02 16:43:29 +02:00
2423f893e5 Update config.json 2021-09-30 14:39:32 +02:00
b6a694afbc master/latest docs differ since --response-as 2021-09-30 14:39:18 +02:00
71adcd97d0 Improve handling of prettified responses without correct content-type encoding (#1110)
* Improve handling of responses without correct content-type charset

* [skip ci] Minor tweaks in tests

* [skip ci] Add documentation

Co-authored-by: claudiatd <claudiatd@gmail.com>

* Improve unknown encoding test

[skip ci]

* Review mime and options retrieval

* Add full content-type example in help output

* Simplify decoder

* [skip ci] s/charset/encoding/

* Tweaks

* [skip ci] Fix type annotation

* [skip ci] s/charset/encoding/

* Tweaks

* Fix type annoation

* Improvement

* Introduce `codec.encode()`

* [skip ci] Tweak changelog

Co-authored-by: claudiatd <claudiatd@gmail.com>
2021-09-29 20:22:19 +02:00
b50f9aa7e7 Use PYthon 3 documentation
[skip ci]
2021-09-28 12:54:16 +02:00
fe96b2af20 Use httpie.io/docs everywhere
[skip ci]
2021-09-28 12:53:53 +02:00
727b8a2c05 Sort available style choices (#1166) 2021-09-27 16:55:10 +02:00
9c89c703ae Allow to overwrite the response Content-Type from options (#1134)
* Allow to override the response `Content-Type` from options

* Apply suggestions from code review

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>

* Rename the option from `--response.content-type` to `--response-as`

* Update CHANGELOG.md

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-09-27 13:58:19 +02:00
8f8851f1db Remove trailing comma in test 2021-09-24 10:37:59 +02:00
bce2b3a98e Sort changelog 2021-09-23 17:17:29 +02:00
474093acdf Include plugin info in --debug output (#1165)
* Include plugin info in `--debug` output

* Adapt issue number

* Fix docs
2021-09-23 17:15:14 +02:00
1535d0c976 Mention XML when explaining formatting 2021-09-23 12:27:03 +02:00
cae83b3f9e Add FreeBSD installation instructions
Closes #761.
2021-09-23 10:46:06 +02:00
507514b795 Add workflow to test with pyOpenSSL active (#1164)
* Add workflow to test with pyOpenSSL active

Original patch by @gmelodie.

* Fix tests on Windows with Python 3.6
2021-09-23 10:37:23 +02:00
d7ed45bbcd Fix duplicate keys preservation of JSON data (#1163)
* Fix duplicate keys preservation of JSON data

* Update issue number

* Fix type annotations

* Changes after review

* Rewording
2021-09-21 19:07:59 +02:00
e6c5cd3e4b Improve JSON output when there is leading data before the actual JSON body (#1130)
In some special cases, to prevent against Cross Site Script Inclusion (XSSI)
attacks, the JSON response body starts with a magic prefix line that must be
stripped before feeding the rest of the response body to the JSON parser.
Such prefix is now simply ignored from the parser but still printed in the
terminal.

* Fix Windows tests
2021-09-21 11:15:43 +02:00
273134123a Bump the version to 2.6.0.dev0 (#1162)
[skip ci]
2021-09-21 10:40:09 +02:00
529aa78ee1 Expand the pytest configuration (#1161)
And rely on it to run tests.
2021-09-20 17:36:03 +02:00
e2ba214ac0 [snap] Improve OS integration (#1157)
Get back read-write access to `$HOME/.config/httpie` and `$HOME/.httpie`.
2021-09-15 16:50:44 +02:00
9dd0203bae Use HTTPie for the documentation build request (#1150)
Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
2021-09-15 14:25:46 +02:00
ba6fd0bc14 Add a blog post link 2021-09-14 01:01:46 +02:00
8f7f4a6ef4 Remove some horizontal lines in the bug report
And add myself to assignees.
2021-09-13 12:36:01 +02:00
9984447f18 Reverse results in bug report temlpate
Ir seems weird to ask for the expected result before knowing the current one.
2021-09-13 12:33:32 +02:00
10081b9fcc Update README.md 2021-09-11 17:17:15 +02:00
4f84362d73 Update config.json 2021-09-10 23:58:33 +02:00
2b5f8f48bf Update update-documentation.yml 2021-09-10 20:06:29 +02:00
a51068a44d Update update-documentation.yml 2021-09-10 20:05:23 +02:00
f06d870012 Update and rename documentations.yml to check-markdown.yml 2021-09-10 20:04:52 +02:00
0115a4a466 Create config.json 2021-09-10 18:50:45 +02:00
7c1d26a8fa Update README.md 2021-09-10 11:17:23 +02:00
7734e47280 Update README.md 2021-09-10 11:17:10 +02:00
30c595b770 [snap] Comment out the problematic interface
It seems it just needs to be present for the snap to be rejected.
2021-09-10 11:04:57 +02:00
b38352858f [snap] Remove personal-files interface
Use of the `personal-files` interface is reserved for vetted publishers.

The interface requires a validation, but we need to publish at least
one package first. So let's skip that part, release a version and ask
for the interface access in a second time.

Also add a workflow to build & test the snap package.
2021-09-10 10:30:44 +02:00
a45b94fda6 Complete CentOS installation instructions 2021-09-09 16:38:36 +02:00
513e5080e4 Add the release workflow
It has to be triggered manually for now.
2021-09-09 16:13:13 +02:00
7c9f415107 Add a workflow to check documentations (#1151)
* Add a workflow to check documentations

* Fix markdown issues

* Install Ruby 2.7

* Finally, handle and fix GitHub templates

* Minor improvement in the feature request template

* Verbose mode to be sure all files are checked
2021-09-09 15:52:24 +02:00
4c8633c6e5 Split the monolithic workflow into specific ones (#1149)
* Split the monolithic workflow into specific ones

* Rename workflows, improve commands

* Update pip from the venv

* Fix Windows setup

* Lowercase macos-latest

* Fix Windows run, again
2021-09-08 16:41:55 +02:00
4d7d6b66cf Trigger official documentation build when documentation is updated here 2021-09-08 15:43:34 +02:00
a586fca246 Update brew formula to 2.5.0 (#1144)
* Update brew formula to 2.5.0

* Can use `idna` 3.2

* Sort requirements to ease reproductible builds

And also to have the same output as `brew bump-formula-pr`.

* Sync `bottles` with official Formula

And keep the `high_sierra` one.

* Add a workflow to check the Formula
2021-09-08 11:01:27 +02:00
978258ec5b Add Alpine Linux installation instructions 2021-09-08 10:04:49 +02:00
84ef9f588c Use lzo compression for snap (#1146) 2021-09-07 16:57:05 +02:00
cf21790411 Add the Snap build file for general Linux packaging
Based on the work of @elopio and @chipaca.

- Added support for the `snapd` protocol URL.
- Packaged Unix socket transport plugin.
2021-09-07 16:45:57 +02:00
1ef127c61d Packit: Get the current Fedora Rawhide specfile
Using the fork is not needed anymore,
since Rawhide was updated to 2.5.0 and no longer has patches.
2021-09-07 11:03:04 +02:00
141 changed files with 8505 additions and 1043 deletions

View File

@ -7,34 +7,38 @@ assignees: ''
---
**Checklist**
## Checklist
- [ ] I've searched for similar issues.
- [ ] I'm using the latest version of HTTPie.
---
**What are the steps to reproduce the problem?**
## Minimal reproduction code and steps
1.
2.
3.
## Current result
**What is the expected result?**
## Expected result
**What happens instead?**
---
**Debug output**
## Debug output
Please re-run the command with `--debug`, then copy the entire command & output and paste both below:
```
```bash
$ http --debug <COMPLETE ARGUMENT LIST THAT TRIGGERS THE ERROR>
<COMPLETE OUTPUT>
```
## Additional information, screenshots, or code examples
**Provide any additional information, screenshots, or code examples below:**

View File

@ -6,19 +6,25 @@ labels: "new, enhancement"
assignees: ''
---
**Checklist**
## Checklist
- [ ] I've searched for similar feature requests.
---
**What enhancement would you like to see?**
## Enhancement request
**What problem does it solve?**
---
E.g. “I'm always frustrated when [...]”, “Im trying to do […] so that […]”.
## Problem it solves
E.g. “I'm always frustrated when […]”, “Im trying to do […] so that […]”.
**Provide any additional information, screenshots, or code examples below:**
---
## Additional information, screenshots, or code examples

52
.github/workflows/benchmark.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Benchmark
on:
pull_request:
types: [ labeled ]
permissions:
issues: write
pull-requests: write
jobs:
test:
if: github.event.label.name == 'benchmark'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.9"
- id: benchmarks
name: Run Benchmarks
run: |
python -m pip install pyperf>=2.3.0
python extras/profiling/run.py --fresh --complex --min-speed=6 --file output.txt
body=$(cat output.txt)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo "::set-output name=body::$body"
- name: Find Comment
uses: peter-evans/find-comment@v1
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '# Benchmarks'
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
# Benchmarks
${{ steps.benchmarks.outputs.body }}
edit-mode: replace
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: benchmark

View File

@ -1,35 +0,0 @@
name: Build
on: [push, pull_request]
jobs:
extras:
# Run coverage and extra tests only once
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: python -m pip install --upgrade pip setuptools wheel
- run: make install
- run: make codestyle
- run: make test-cover
- run: make codecov-upload
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_REPO_TOKEN }}
- run: make test-dist
test:
# Run core HTTPie tests everywhere
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9, "3.10.0-rc.1"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip setuptools wheel
- run: python -m pip install --upgrade '.[dev]'
- run: python -m pytest --verbose ./httpie ./tests

19
.github/workflows/code-style.yml vendored Normal file
View 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
View 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

View 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
View 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 }}

View File

@ -0,0 +1,31 @@
on:
push:
branches:
- master
paths:
- .github/workflows/docs-update-install.yml
- docs/installation/*
# Allow to call the workflow manually
workflow_dispatch:
jobs:
doc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: make install
- run: make doc-update-install
- uses: Automattic/action-commit-to-branch@master
with:
branch: master
commit_message: |
Auto-update install docs
Via .github/workflows/docs-update-install.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

22
.github/workflows/release-snap.yml vendored Normal file
View File

@ -0,0 +1,22 @@
on:
workflow_dispatch:
inputs:
branch:
description: "The branch, tag or SHA to release from"
required: true
default: "master"
jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch }}
- uses: snapcore/action-build@v1
id: build
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAP_STORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge

31
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
on:
# Add a "Trigger" button to manually start the workflow.
workflow_dispatch:
inputs:
branch:
description: "The branch, tag or SHA to release from"
required: true
default: "master"
# It could be fully automated by uncommenting following lines.
# Let's see later if we are confident enough to try it :)
# release:
# types:
# - published
jobs:
new-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch }}
- name: PyPi configuration
run: |
echo "[distutils]\nindex-servers=\n httpie\n\n[httpie]\nrepository = https://upload.pypi.org/legacy/\n" > $HOME/.pypirc
- uses: actions/setup-python@v2
with:
python-version: 3.9
- run: make publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

26
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: Mark stale pull requests
on: workflow_dispatch
permissions:
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
close-pr-message: 'Thanks for the pull request, but since it was stale for more than a 30 days we are closing it. If you want to work back on it, feel free to re-open it or create a new one.'
stale-pr-label: 'stale'
days-before-stale: -1
days-before-issue-stale: -1
days-before-pr-stale: 30
days-before-close: -1
days-before-issue-close: -1
days-before-pr-close: 0
operations-per-run: 300

View File

@ -0,0 +1,25 @@
on:
pull_request:
paths:
- .github/workflows/test-package-linux-snap.yml
- snapcraft.yaml
workflow_dispatch:
jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
uses: snapcore/action-build@v1
id: snapcraft
- name: Install
run: sudo snap install --dangerous ${{ steps.snapcraft.outputs.snap }}
- name: Test
run: |
httpie.http --version
httpie.https --version
httpie --version
# Auto-aliases cannot be tested when installing a snap outside the store.
# http --version
# https --version

View File

@ -0,0 +1,18 @@
on:
pull_request:
paths:
- .github/workflows/test-package-mac-brew.yml
- docs/packaging/brew/httpie.rb
workflow_dispatch:
jobs:
brew:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Setup brew
run: |
brew developer on
brew update
- name: Build and test the formula
run: make brew-test

45
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,45 @@
on:
push:
branches:
- master
paths:
- .github/workflows/tests.yml
- httpie/**/*.py
- setup.*
- tests/**/*.py
pull_request:
paths:
- .github/workflows/tests.yml
- httpie/**/*.py
- setup.*
- tests/**/*.py
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10"]
pyopenssl: [0, 1]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Windows setup
if: matrix.os == 'windows-latest'
run: |
python -m pip install --upgrade pip wheel
python -m pip install --upgrade '.[dev]'
python -m pytest --verbose ./httpie ./tests
env:
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}
- name: Linux & Mac setup
if: matrix.os != 'windows-latest'
run: |
make install
make test
env:
HTTPIE_TEST_WITH_PYOPENSSL: ${{ matrix.pyopenssl }}

7
.gitignore vendored
View File

@ -118,6 +118,7 @@ celerybeat.pid
.venv
env/
venv/
venv*/
ENV/
env.bak/
venv.bak/
@ -144,3 +145,9 @@ dmypy.json
/httpie.spec
/httpie-*.rpm
/httpie-*.tar.gz
# VS Code
.vscode/
# Windows Chocolatey
*.nupkg

View File

@ -2,10 +2,9 @@
# https://packit.dev/docs/configuration/
specfile_path: httpie.spec
actions:
# the current Fedora Rawhide specfile has some patches
# so we get it from @hroncok's (= churchyard in Fedora) fork for now
# once we have a new release, we'll use: https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec
post-upstream-clone: "wget https://src.fedoraproject.org/fork/churchyard/rpms/httpie/raw/packit/f/httpie.spec -O httpie.spec"
# get the current Fedora Rawhide specfile:
# post-upstream-clone: "wget https://src.fedoraproject.org/rpms/httpie/raw/rawhide/f/httpie.spec -O httpie.spec"
post-upstream-clone: "cp docs/packaging/linux-fedora/httpie.spec.txt httpie.spec"
jobs:
- job: copr_build
trigger: pull_request

View File

@ -3,8 +3,45 @@
This document records all notable changes to [HTTPie](https://httpie.io).
This project adheres to [Semantic Versioning](https://semver.org/).
## [3.0.0](https://github.com/httpie/httpie/compare/2.6.0...3.0.0) (2022-01-21)
- Dropped support for Python 3.6. ([#1177](https://github.com/httpie/httpie/issues/1177))
- Improved startup time by 40%. ([#1211](https://github.com/httpie/httpie/pull/1211))
- Added support for nested JSON syntax. ([#1169](https://github.com/httpie/httpie/issues/1169))
- Added `httpie plugins` interface for plugin management. ([#566](https://github.com/httpie/httpie/issues/566))
- Added support for Bearer authentication via `--auth-type=bearer` ([#1215](https://github.com/httpie/httpie/issues/1215)).
- Added support for quick conversions of pasted URLs into HTTPie calls by adding a space after the protocol name (`$ https ://pie.dev``https://pie.dev`). ([#1195](https://github.com/httpie/httpie/issues/1195))
- Added support for _sending_ multiple HTTP header lines with the same name. ([#130](https://github.com/httpie/httpie/issues/130))
- Added support for _receiving_ multiple HTTP headers lines with the same name. ([#1207](https://github.com/httpie/httpie/issues/1207))
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))
- Added support for automatically enabling `--stream` when `Content-Type` is `text/event-stream`. ([#376](https://github.com/httpie/httpie/issues/376))
- Added support for displaying the total elapsed time through `--meta`/`-vv` or `--print=m`. ([#243](https://github.com/httpie/httpie/issues/243))
- Added new `pie-dark`/`pie-light` (and `pie`) styles that match with [HTTPie for Web and Desktop](https://httpie.io/product). ([#1237](https://github.com/httpie/httpie/issues/1237))
- Added support for better error handling on DNS failures. ([#1248](https://github.com/httpie/httpie/issues/1248))
- Added support for storing prompted passwords in the local sessions. ([#1098](https://github.com/httpie/httpie/issues/1098))
- Added warnings about the `--ignore-stdin`, when there is no incoming data from stdin. ([#1255](https://github.com/httpie/httpie/issues/1255))
- Fixed crashing due to broken plugins. ([#1204](https://github.com/httpie/httpie/issues/1204))
- Fixed auto addition of XML declaration to every formatted XML response. ([#1156](https://github.com/httpie/httpie/issues/1156))
- Fixed highlighting when `Content-Type` specifies `charset`. ([#1242](https://github.com/httpie/httpie/issues/1242))
- Fixed an unexpected crash when `--raw` is used with `--chunked`. ([#1253](https://github.com/httpie/httpie/issues/1253))
- Changed the default Windows theme from `fruity` to `auto`. ([#1266](https://github.com/httpie/httpie/issues/1266))
## [2.6.0](https://github.com/httpie/httpie/compare/2.5.0...2.6.0) (2021-10-14)
[Whats new in HTTPie 2.6.0 →](https://httpie.io/blog/httpie-2.6.0)
- Added support for formatting & coloring of JSON bodies preceded by non-JSON data (e.g., an XXSI prefix). ([#1130](https://github.com/httpie/httpie/issues/1130))
- Added charset auto-detection when `Content-Type` doesnt include it. ([#1110](https://github.com/httpie/httpie/issues/1110), [#1168](https://github.com/httpie/httpie/issues/1168))
- Added `--response-charset` to allow overriding the response encoding for terminal display purposes. ([#1168](https://github.com/httpie/httpie/issues/1168))
- Added `--response-mime` to allow overriding the response mime type for coloring and formatting for the terminal. ([#1168](https://github.com/httpie/httpie/issues/1168))
- Added the ability to silence warnings through using `-q` or `--quiet` twice (e.g. `-qq`) ([#1175](https://github.com/httpie/httpie/issues/1175))
- Added installed plugin list to `--debug` output. ([#1165](https://github.com/httpie/httpie/issues/1165))
- Fixed duplicate keys preservation in JSON data. ([#1163](https://github.com/httpie/httpie/issues/1163))
## [2.5.0](https://github.com/httpie/httpie/compare/2.4.0...2.5.0) (2021-09-06)
[Whats 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))

View File

@ -68,7 +68,7 @@ members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
<https://www.contributor-covenant.org/faq>

View File

@ -44,7 +44,7 @@ Consider also adding a [CHANGELOG](https://github.com/httpie/httpie/blob/master/
#### Getting the code
Go to https://github.com/httpie/httpie and fork the project repository.
Go to <https://github.com/httpie/httpie> and fork the project repository.
```bash
# Clone your fork
@ -89,7 +89,7 @@ a hack but it works™.)
You should now see `(httpie)` next to your shell prompt, and
the `http` command should point to your development copy:
```
```bash
(httpie) ~/Code/httpie $ which http
/Users/<user>/Code/httpie/venv/bin/http
(httpie) ~/Code/httpie $ http --version
@ -144,11 +144,25 @@ $ python -m pytest tests/test_uploads.py::TestMultipartFormDataFileUpload::test_
See [Makefile](https://github.com/httpie/httpie/blob/master/Makefile) for additional development utilities.
#### Running benchmarks
If you are trying to work on speeding up HTTPie and want to verify your results, you
can run the benchmark suite. The suite will compare the last commit of your branch
with the master branch of your repository (or a fresh checkout of HTTPie master, through
`--fresh`) and report the results back.
```bash
$ python extras/benchmarks/run.py
```
The benchmarks can also be run on the CI. Since it is a long process, it requires manual
oversight. Ping one of the maintainers to get a `benchmark` label on your branch.
#### Windows
If you are on a Windows machine and not able to run `make`,
follow the next steps for a basic setup. As a prerequisite, you need to have
Python 3.6+ installed.
Python 3.7+ installed.
Create a virtual environment and activate it:
@ -160,7 +174,7 @@ C:\> venv\Scripts\activate
Install HTTPie in editable mode with all the dependencies:
```powershell
C:\> python -m pip install --upgrade -e . -r requirements-dev.txt
C:\> python -m pip install --upgrade -e .[dev]
```
You should now see `(httpie)` next to your shell prompt, and
@ -168,19 +182,19 @@ the `http` command should point to your development copy:
```powershell
# In PowerShell:
(httpie) PS C:\Users\ovezovs\httpie> Get-Command http
(httpie) PS C:\Users\<user>\httpie> Get-Command http
CommandType Name Version Source
----------- ---- ------- ------
Application http.exe 0.0.0.0 C:\Users\ovezovs\httpie\venv\Scripts\http.exe
Application http.exe 0.0.0.0 C:\Users\<user>\httpie\venv\Scripts\http.exe
```
```bash
# In CMD:
(httpie) C:\Users\ovezovs\httpie> where http
C:\Users\ovezovs\httpie\venv\Scripts\http.exe
C:\Users\ovezovs\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
(httpie) C:\Users\<user>\httpie> where http
C:\Users\<user>\httpie\venv\Scripts\http.exe
C:\Users\<user>\AppData\Local\Programs\Python\Python38-32\Scripts\http.exe
(httpie) C:\Users\ovezovs\httpie> http --version
(httpie) C:\Users\<user>\httpie> http --version
2.3.0-dev
```

View File

@ -25,7 +25,13 @@ export PATH := $(VENV_BIN):$(PATH)
all: uninstall-httpie install test
install: venv
install: venv install-reqs
install-reqs:
@echo $(H1)Updating package tools$(H1END)
$(VENV_PIP) install --upgrade pip wheel
@echo $(H1)Installing dev requirements$(H1END)
$(VENV_PIP) install --upgrade --editable '.[dev]'
@ -34,6 +40,7 @@ install: venv
@echo
clean:
@echo $(H1)Cleaning up$(H1END)
rm -rf $(VENV_ROOT)
@ -77,11 +84,11 @@ venv:
test:
@echo $(H1)Running tests$(HEADER_EXTRA)$(H1END)
$(VENV_BIN)/python -m pytest $(COV) ./httpie $(COV) ./tests --doctest-modules --verbose ./httpie ./tests
$(VENV_BIN)/python -m pytest $(COV)
@echo
test-cover: COV=--cov
test-cover: COV=--cov=httpie --cov=tests
test-cover: HEADER_EXTRA=' (with coverage)'
test-cover: test
@ -123,7 +130,7 @@ pycodestyle: codestyle
codestyle:
@echo $(H1)Running flake8$(H1END)
@[ -f $(VENV_BIN)/flake8 ] || $(VENV_PIP) install --upgrade --editable '.[dev]'
$(VENV_BIN)/flake8 httpie/ tests/ extras/ *.py
$(VENV_BIN)/flake8 httpie/ tests/ extras/profiling/ docs/packaging/brew/ *.py
@echo
@ -135,6 +142,16 @@ codecov-upload:
@echo
doc-check:
@echo $(H1)Running documentations checks$(H1END)
mdl --git-recurse --style docs/markdownlint.rb .
doc-update-install:
@echo $(H1)Updating installation instructions in the docs$(H1END)
$(VENV_PYTHON) docs/installation/generate.py
###############################################################################
# Publishing to PyPi
###############################################################################
@ -179,10 +196,17 @@ uninstall-httpie:
###############################################################################
brew-deps:
extras/brew-deps.py
docs/packaging/brew/brew-deps.py
brew-test:
@echo $(H1)Uninstalling httpie$(H1END)
- brew uninstall httpie
brew install --build-from-source ./extras/httpie.rb
@echo $(H1)Building from source…$(H1END)
- brew install --build-from-source ./docs/packaging/brew/httpie.rb
@echo $(H1)Verifying…$(H1END)
brew test httpie
@echo $(H1)Auditing…$(H1END)
brew audit --strict httpie

View File

@ -17,7 +17,7 @@ They use simple and natural syntax and provide formatted and colorized output.
[![Build](https://img.shields.io/github/workflow/status/httpie/httpie/Build?color=%23FA9BFA&label=Build)](https://github.com/httpie/httpie/actions)
[![Coverage](https://img.shields.io/codecov/c/github/httpie/httpie?style=flat&label=Coverage&color=%2373DC8C)](https://codecov.io/gh/httpie/httpie)
[![Twitter](https://img.shields.io/twitter/follow/httpie?style=flat&color=%234B78E6&logoColor=%234B78E6)](https://twitter.com/httpie)
[![Chat](https://img.shields.io/badge/chat-Discord-brightgreen?style=flat&label=Chat%20on&color=%23FA9BFA)](https://httpie.io/chat)
[![Chat](https://img.shields.io/badge/chat-Discord-brightgreen?style=flat&label=Chat%20on&color=%23FA9BFA)](https://httpie.io/discord)
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
@ -38,31 +38,31 @@ They use simple and natural syntax and provide formatted and colorized output.
- Persistent sessions
- `wget`-like downloads
[See for all features →](https://httpie.io/docs)
[See all features →](https://httpie.io/docs)
## Examples
Hello World:
```
```bash
$ https httpie.io/hello
```
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
```
```bash
$ http PUT pie.dev/put X-API-Token:123 name=John
```
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
```
```bash
$ http --offline pie.dev/post hello=offline
```
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
```
```bash
$ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
```
@ -71,7 +71,7 @@ $ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/com
## Community & support
- Visit the [HTTPie website](https://httpie.io) for full documentation and useful links.
- Join our [Discord server](https://httpie.io/chat) is to ask questions, discuss features, and for general API chat.
- Join our [Discord server](https://httpie.io/discord) is to ask questions, discuss features, and for general API chat.
- Tweet at [@httpie](https://twitter.com/httpie) on Twitter.
- Use [StackOverflow](https://stackoverflow.com/questions/tagged/httpie) to ask questions and include a `httpie` tag.
- Create [GitHub Issues](https://github.com/httpie/httpie/issues) for bug reports and feature requests.

File diff suppressed because it is too large Load Diff

5
docs/config.json Normal file
View File

@ -0,0 +1,5 @@
{
"website": {
"master_and_released_docs_differ_after": "d40f06687f8cbbd22bf7dba05bee93aea11a169f"
}
}

View File

@ -0,0 +1,3 @@
Here we maintain a database of contributors, from which we generate credits on release blog posts and social medias.
For the HTTPie blog see: <https://httpie.io/blog>.

280
docs/contributors/fetch.py Normal file
View File

@ -0,0 +1,280 @@
"""
Generate the contributors database.
FIXME: replace `requests` calls with the HTTPie API, when available.
"""
import json
import os
import re
import sys
from copy import deepcopy
from datetime import datetime
from pathlib import Path
from subprocess import check_output
from time import sleep
from typing import Any, Dict, Optional, Set
import requests
FullNames = Set[str]
GitHubLogins = Set[str]
Person = Dict[str, str]
People = Dict[str, Person]
UserInfo = Dict[str, Any]
CO_AUTHORS = re.compile(r'Co-authored-by: ([^<]+) <').finditer
API_URL = 'https://api.github.com'
REPO = OWNER = 'httpie'
REPO_URL = f'{API_URL}/repos/{REPO}/{OWNER}'
HERE = Path(__file__).parent
DB_FILE = HERE / 'people.json'
DEFAULT_PERSON: Person = {'committed': [], 'reported': [], 'github': '', 'twitter': ''}
SKIPPED_LABELS = {'invalid'}
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
assert GITHUB_TOKEN, 'GITHUB_TOKEN envar is missing'
class FinishedForNow(Exception):
"""Raised when remaining GitHub rate limit is zero."""
def main(previous_release: str, current_release: str) -> int:
since = release_date(previous_release)
until = release_date(current_release)
contributors = load_awesome_people()
try:
committers = find_committers(since, until)
reporters = find_reporters(since, until)
except Exception as exc:
# We want to save what we fetched so far. So pass.
print(' !! ', exc)
try:
merge_all_the_people(current_release, contributors, committers, reporters)
fetch_missing_users_details(contributors)
except FinishedForNow:
# We want to save what we fetched so far. So pass.
print(' !! Committers:', committers)
print(' !! Reporters:', reporters)
exit_status = 1
else:
exit_status = 0
save_awesome_people(contributors)
return exit_status
def find_committers(since: str, until: str) -> FullNames:
url = f'{REPO_URL}/commits'
page = 1
per_page = 100
params = {
'since': since,
'until': until,
'per_page': per_page,
}
committers: FullNames = set()
while 'there are commits':
params['page'] = page
data = fetch(url, params=params)
for item in data:
commit = item['commit']
committers.add(commit['author']['name'])
debug(' >>> Commit', item['html_url'])
for co_author in CO_AUTHORS(commit['message']):
name = co_author.group(1)
committers.add(name)
if len(data) < per_page:
break
page += 1
return committers
def find_reporters(since: str, until: str) -> GitHubLogins:
url = f'{API_URL}/search/issues'
page = 1
per_page = 100
params = {
'q': f'repo:{REPO}/{OWNER} is:issue closed:{since}..{until}',
'per_page': per_page,
}
reporters: GitHubLogins = set()
while 'there are issues':
params['page'] = page
data = fetch(url, params=params)
for item in data['items']:
# Filter out unwanted labels.
if any(label['name'] in SKIPPED_LABELS for label in item['labels']):
continue
debug(' >>> Issue', item['html_url'])
reporters.add(item['user']['login'])
if len(data['items']) < per_page:
break
page += 1
return reporters
def merge_all_the_people(release: str, contributors: People, committers: FullNames, reporters: GitHubLogins) -> None:
"""
>>> contributors = {'Alice': new_person(github='alice', twitter='alice')}
>>> merge_all_the_people('2.6.0', contributors, {}, {})
>>> contributors
{'Alice': {'committed': [], 'reported': [], 'github': 'alice', 'twitter': 'alice'}}
>>> contributors = {'Bob': new_person(github='bob', twitter='bob')}
>>> merge_all_the_people('2.6.0', contributors, {'Bob'}, {'bob'})
>>> contributors
{'Bob': {'committed': ['2.6.0'], 'reported': ['2.6.0'], 'github': 'bob', 'twitter': 'bob'}}
>>> contributors = {'Charlotte': new_person(github='charlotte', twitter='charlotte', committed=['2.5.0'], reported=['2.5.0'])}
>>> merge_all_the_people('2.6.0', contributors, {'Charlotte'}, {'charlotte'})
>>> contributors
{'Charlotte': {'committed': ['2.5.0', '2.6.0'], 'reported': ['2.5.0', '2.6.0'], 'github': 'charlotte', 'twitter': 'charlotte'}}
"""
# Update known contributors.
for name, details in contributors.items():
if name in committers:
if release not in details['committed']:
details['committed'].append(release)
committers.remove(name)
if details['github'] in reporters:
if release not in details['reported']:
details['reported'].append(release)
reporters.remove(details['github'])
# Add new committers.
for name in committers:
user_info = user(fullname=name)
contributors[name] = new_person(
github=user_info['login'],
twitter=user_info['twitter_username'],
committed=[release],
)
if user_info['login'] in reporters:
contributors[name]['reported'].append(release)
reporters.remove(user_info['login'])
# Add new reporters.
for github_username in reporters:
user_info = user(github_username=github_username)
contributors[user_info['name'] or user_info['login']] = new_person(
github=github_username,
twitter=user_info['twitter_username'],
reported=[release],
)
def release_date(release: str) -> str:
date = check_output(['git', 'log', '-1', '--format=%ai', release], text=True).strip()
return datetime.strptime(date, '%Y-%m-%d %H:%M:%S %z').isoformat()
def load_awesome_people() -> People:
try:
with DB_FILE.open(encoding='utf-8') as fh:
return json.load(fh)
except (FileNotFoundError, ValueError):
return {}
def fetch(url: str, params: Optional[Dict[str, str]] = None) -> UserInfo:
headers = {
'Accept': 'application/vnd.github.v3+json',
'Authentication': f'token {GITHUB_TOKEN}'
}
for retry in range(1, 6):
debug(f'[{retry}/5]', f'{url = }', f'{params = }')
with requests.get(url, params=params, headers=headers) as req:
try:
req.raise_for_status()
except requests.exceptions.HTTPError as exc:
if exc.response.status_code == 403:
# 403 Client Error: rate limit exceeded for url: ...
now = int(datetime.utcnow().timestamp())
xrate_limit_reset = int(exc.response.headers['X-RateLimit-Reset'])
wait = xrate_limit_reset - now
if wait > 20:
raise FinishedForNow()
debug(' !', 'Waiting', wait, 'seconds before another try ...')
sleep(wait)
continue
return req.json()
assert ValueError('Rate limit exceeded')
def new_person(**kwargs: str) -> Person:
data = deepcopy(DEFAULT_PERSON)
data.update(**kwargs)
return data
def user(fullname: Optional[str] = '', github_username: Optional[str] = '') -> UserInfo:
if github_username:
url = f'{API_URL}/users/{github_username}'
return fetch(url)
url = f'{API_URL}/search/users'
for query in (f'fullname:{fullname}', f'user:{fullname}'):
params = {
'q': f'repo:{REPO}/{OWNER} {query}',
'per_page': 1,
}
user_info = fetch(url, params=params)
if user_info['items']:
user_url = user_info['items'][0]['url']
return fetch(user_url)
def fetch_missing_users_details(people: People) -> None:
for name, details in people.items():
if details['github'] and details['twitter']:
continue
user_info = user(github_username=details['github'], fullname=name)
if not details['github']:
details['github'] = user_info['login']
if not details['twitter']:
details['twitter'] = user_info['twitter_username']
def save_awesome_people(people: People) -> None:
with DB_FILE.open(mode='w', encoding='utf-8') as fh:
json.dump(people, fh, indent=4, sort_keys=True)
def debug(*args: Any) -> None:
if os.getenv('DEBUG') == '1':
print(*args)
if __name__ == '__main__':
ret = 1
try:
ret = main(*sys.argv[1:])
except TypeError:
ret = 2
print(f'''
Fetch contributors to a release.
Usage:
python {sys.argv[0]} {sys.argv[0]} <RELEASE N-1> <RELEASE N>
Example:
python {sys.argv[0]} 2.4.0 2.5.0
Define the DEBUG=1 environment variable to enable verbose output.
''')
except KeyboardInterrupt:
ret = 255
sys.exit(ret)

View File

@ -0,0 +1,45 @@
"""
Generate snippets to copy-paste.
"""
import sys
from jinja2 import Template
from fetch import HERE, load_awesome_people
TPL_FILE = HERE / 'snippet.jinja2'
HTTPIE_TEAM = {
'claudiatd',
'jakubroztocil',
'jkbr',
}
def generate_snippets(release: str) -> str:
people = load_awesome_people()
contributors = {
name: details
for name, details in people.items()
if details['github'] not in HTTPIE_TEAM
and (release in details['committed'] or release in details['reported'])
}
template = Template(source=TPL_FILE.read_text(encoding='utf-8'))
output = template.render(contributors=contributors, release=release)
print(output)
return 0
if __name__ == '__main__':
ret = 1
try:
ret = generate_snippets(sys.argv[1])
except (IndexError, TypeError):
ret = 2
print(f'''
Generate snippets for contributors to a release.
Usage:
python {sys.argv[0]} {sys.argv[0]} <RELEASE>
''')
sys.exit(ret)

View File

@ -0,0 +1,329 @@
{
"Almad": {
"committed": [
"2.5.0"
],
"github": "Almad",
"reported": [
"2.6.0"
],
"twitter": "almadcz"
},
"Annette Wilson": {
"committed": [],
"github": "annettejanewilson",
"reported": [
"2.6.0"
],
"twitter": null
},
"Anton Emelyanov": {
"committed": [
"2.5.0"
],
"github": "king-menin",
"reported": [],
"twitter": null
},
"D8ger": {
"committed": [],
"github": "caofanCPU",
"reported": [
"2.5.0"
],
"twitter": null
},
"Dave": {
"committed": [
"2.6.0"
],
"github": "davecheney",
"reported": [],
"twitter": "davecheney"
},
"Dawid Ferenczy Rogo\u017ean": {
"committed": [],
"github": "ferenczy",
"reported": [
"2.5.0"
],
"twitter": "DawidFerenczy"
},
"Elena Lape": {
"committed": [
"2.5.0"
],
"github": "elenalape",
"reported": [],
"twitter": "elena_lape"
},
"Fabio Peruzzo": {
"committed": [],
"github": "peruzzof",
"reported": [
"2.6.0"
],
"twitter": null
},
"F\u00fash\u0113ng": {
"committed": [],
"github": "lienide",
"reported": [
"2.5.0"
],
"twitter": null
},
"Giampaolo Rodola": {
"committed": [],
"github": "giampaolo",
"reported": [
"2.5.0"
],
"twitter": null
},
"Hugh Williams": {
"committed": [],
"github": "hughpv",
"reported": [
"2.5.0"
],
"twitter": null
},
"Ilya Sukhanov": {
"committed": [
"2.5.0"
],
"github": "IlyaSukhanov",
"reported": [
"2.5.0"
],
"twitter": null
},
"Jakub Roztocil": {
"committed": [
"2.5.0",
"2.6.0"
],
"github": "jakubroztocil",
"reported": [
"2.5.0",
"2.6.0"
],
"twitter": "jakubroztocil"
},
"Jan Verbeek": {
"committed": [
"2.5.0"
],
"github": "blyxxyz",
"reported": [],
"twitter": null
},
"Jannik Vieten": {
"committed": [
"2.5.0"
],
"github": "exploide",
"reported": [],
"twitter": null
},
"Marcel St\u00f6r": {
"committed": [
"2.5.0"
],
"github": "marcelstoer",
"reported": [],
"twitter": "frightanic"
},
"Mariano Ruiz": {
"committed": [],
"github": "mrsarm",
"reported": [
"2.5.0"
],
"twitter": "mrsarm82"
},
"Micka\u00ebl Schoentgen": {
"committed": [
"2.5.0",
"2.6.0"
],
"github": "BoboTiG",
"reported": [
"2.5.0",
"2.6.0"
],
"twitter": "__tiger222__"
},
"Miro Hron\u010dok": {
"committed": [
"2.5.0",
"2.6.0"
],
"github": "hroncok",
"reported": [],
"twitter": "hroncok"
},
"Mohamed Daahir": {
"committed": [],
"github": "ducaale",
"reported": [
"2.5.0"
],
"twitter": null
},
"Omer Akram": {
"committed": [
"2.6.0"
],
"github": "om26er",
"reported": [
"2.6.0"
],
"twitter": "om26er"
},
"Pavel Alexeev aka Pahan-Hubbitus": {
"committed": [],
"github": "Hubbitus",
"reported": [
"2.5.0"
],
"twitter": null
},
"Samuel Marks": {
"committed": [],
"github": "SamuelMarks",
"reported": [
"2.5.0"
],
"twitter": null
},
"Sullivan SENECHAL": {
"committed": [],
"github": "soullivaneuh",
"reported": [
"2.5.0"
],
"twitter": null
},
"Thomas Klinger": {
"committed": [],
"github": "mosesontheweb",
"reported": [
"2.5.0"
],
"twitter": null
},
"Vincent van \u2019t Zand": {
"committed": [],
"github": "vovtz",
"reported": [
"2.6.0"
],
"twitter": null
},
"Yannic Schneider": {
"committed": [],
"github": "cynay",
"reported": [
"2.5.0"
],
"twitter": null
},
"a1346054": {
"committed": [
"2.5.0"
],
"github": "a1346054",
"reported": [],
"twitter": null
},
"bl-ue": {
"committed": [
"2.5.0"
],
"github": "FiReBlUe45",
"reported": [],
"twitter": null
},
"claudiatd": {
"committed": [
"2.6.0"
],
"github": "claudiatd",
"reported": [],
"twitter": null
},
"dkreeft": {
"committed": [
"2.6.0"
],
"github": "dkreeft",
"reported": [],
"twitter": null
},
"henryhu712": {
"committed": [
"2.5.0"
],
"github": "henryhu712",
"reported": [],
"twitter": null
},
"jakubroztocil": {
"committed": [
"2.6.0"
],
"github": "jkbr",
"reported": [],
"twitter": null
},
"jungle-boogie": {
"committed": [],
"github": "jungle-boogie",
"reported": [
"2.5.0"
],
"twitter": null
},
"nixbytes": {
"committed": [
"2.5.0"
],
"github": "nixbytes",
"reported": [],
"twitter": "linuxbyte3"
},
"qiulang": {
"committed": [],
"github": "qiulang",
"reported": [
"2.5.0"
],
"twitter": null
},
"zwx00": {
"committed": [],
"github": "zwx00",
"reported": [
"2.5.0"
],
"twitter": null
},
"\u5d14\u5c0f\u4e8c": {
"committed": [],
"github": "rogerdehe",
"reported": [
"2.6.0"
],
"twitter": null
},
"\u9ec4\u6d77": {
"committed": [],
"github": "hh-in-zhuzhou",
"reported": [
"2.6.0"
],
"twitter": null
}
}

View File

@ -0,0 +1,13 @@
<!-- Blog post -->
## Community contributions
Wed like to thank these amazing people for their contributions to this release: {% for name, details in contributors.items() -%}
[{{ name }}](https://github.com/{{ details.github }}){{ '' if loop.last else ', ' }}
{%- endfor %}.
<!-- Twitter -->
Wed like to thank these amazing people for their contributions to HTTPie {{ release }}: {% for name, details in contributors.items() if details.twitter -%}
@{{ details.twitter }}{{ '' if loop.last else ', ' }}
{%- endfor %} 🥧

View File

@ -0,0 +1,5 @@
Here we maintain a database of installation methods, from which we generate
the installation section in docs. If youd 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>.

View 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())

View File

@ -0,0 +1,37 @@
<!--
THE INSTALLATION SECTION IS GENERATED
Do not edit here, but in docs/installation/.
-->
{% for platform, tools in structure %}
- [{{ platform }}](#{{ platform.lower() }}){% endfor %} {# <= keep `endfor` here to prevent unwanted `\n` #}
{% for platform, tools in structure %}
### {{ platform }}
{% for tool in tools %}
#### {{ tool.title }}
{% if tool.note %}
{{ tool.note }}
{% endif %}
{% if tool.links.setup %}
To install [{{ tool.name }}]({{ tool.links.homepage }}), see [its installation]({{ tool.links.setup }}).
{% endif %}
```bash
# Install httpie
$ {{ tool.commands.install|join('\n$ ') }}
```
```bash
# Upgrade httpie
$ {{ tool.commands.upgrade|join('\n$ ') }}
```
{% endfor %}
{% endfor %}
<!-- /GENERATED SECTION -->

View File

@ -0,0 +1,181 @@
# Database of HTTPie installation methods. Used to build the docs.
#
# We currently only include here methods for popular systems where we take care of the package,
# or have a good relationship with the maintainers.
#
# Each tool name should be unique (it becomes a linkable header).
# If a tools have `links.setup`, it also needs `links.homepage`.
# Some tools are available on multiple platforms, take into account when editing.
#
docs-structure:
Universal:
- pypi
macOS:
- brew-mac
- port
Windows:
- chocolatey
Linux:
- snap-linux
- brew-linux
- apt
- dnf
- yum
- pacman
FreeBSD:
- pkg
tools:
apt:
title: Debian and Ubuntu
note: Also works for other Debian-derived distributions like MX Linux, Linux Mint, deepin, Pop!_OS, KDE neon, Zorin OS, elementary OS, Kubuntu, Devuan, Linux Lite, Peppermint OS, Lubuntu, antiX, Xubuntu, etc.
name: APT
links:
homepage: https://en.wikipedia.org/wiki/APT_(software)
package: https://packages.debian.org/sid/web/httpie
commands:
install:
- apt update
- apt install httpie
upgrade:
- apt update
- apt upgrade httpie
brew-mac:
title: Homebrew
name: Homebrew
links:
homepage: https://brew.sh/
setup: https://docs.brew.sh/Installation
package: https://formulae.brew.sh/formula/httpie
commands:
install:
- brew update
- brew install httpie
upgrade:
- brew update
- brew upgrade httpie
brew-linux:
title: Linuxbrew
name: Linuxbrew
links:
homepage: https://docs.brew.sh/Homebrew-on-Linux
setup: https://docs.brew.sh/Homebrew-on-Linux#install
package: https://formulae.brew.sh/formula/httpie
commands:
install:
- brew update
- brew install httpie
upgrade:
- brew update
- brew upgrade httpie
chocolatey:
title: Chocolatey
name: Chocolatey
links:
homepage: https://chocolatey.org/
setup: https://chocolatey.org/install
package: https://community.chocolatey.org/packages/httpie/
commands:
install:
- choco install httpie
upgrade:
- choco upgrade httpie
dnf:
title: Fedora
name: DNF
links:
homepage: https://fedoraproject.org/wiki/DNF
package: https://src.fedoraproject.org/rpms/httpie
commands:
install:
- dnf install httpie
upgrade:
- dnf upgrade httpie
pacman:
title: Arch Linux
name: pacman
note: Also works for other Arch-derived distributions like ArcoLinux, EndeavourOS, Artix Linux, etc.
links:
homepage: https://archlinux.org/pacman/
package: https://archlinux.org/packages/community/any/httpie/
commands:
install:
- pacman -Sy httpie
upgrade:
- pacman -Syu httpie
pkg:
title: FreshPorts
name: FreshPorts
links:
homepage: https://www.freebsd.org/cgi/man.cgi?query=pkg&sektion=8&n=1
package: https://www.freshports.org/www/py-httpie/
commands:
install:
- pkg install www/py-httpie
upgrade:
- pkg upgrade www/py-httpie
port:
title: MacPorts
name: MacPorts
links:
homepage: https://www.macports.org/
setup: https://www.macports.org/install.php
package: https://ports.macports.org/port/httpie/
commands:
install:
- port selfupdate
- port install httpie
upgrade:
- port selfupdate
- port upgrade httpie
pypi:
title: PyPI
name: pip
note: Please make sure you have Python 3.7 or newer (`python --version`).
links:
homepage: https://pypi.org/
# setup: https://pip.pypa.io/en/stable/installation/
package: https://pypi.org/project/httpie/
commands:
install:
- python -m pip install --upgrade pip wheel
- python -m pip install httpie
upgrade:
- python -m pip install --upgrade pip wheel
- python -m pip install --upgrade httpie
snap-linux:
title: Snapcraft (Linux)
name: Snapcraft
links:
homepage: https://snapcraft.io/
setup: https://snapcraft.io/docs/installing-snapd
package: https://snapcraft.io/httpie
commands:
install:
- snap install httpie
upgrade:
- snap refresh httpie
yum:
title: CentOS and RHEL
name: Yum
note: Also works for other RHEL-derived distributions like ClearOS, Oracle Linux, etc.
links:
homepage: http://yum.baseurl.org/
package: https://src.fedoraproject.org/rpms/httpie
commands:
install:
- yum install epel-release
- yum install httpie
upgrade:
- yum upgrade httpie

44
docs/markdownlint.rb Normal file
View File

@ -0,0 +1,44 @@
# Rules for <https://github.com/markdownlint/markdownlint>
# Load all rules by default
all
#
# Tweak rules
#
# MD002 First header should be a top level header
# Because we use HTML to hide them on the website.
exclude_rule 'MD002'
# MD013 Line length
exclude_rule 'MD013'
# MD014 Dollar signs used before commands without showing output
exclude_rule 'MD014'
# MD028 Blank line inside blockquote
exclude_rule 'MD028'
# Tell the linter to use ordered lists:
# 1. Foo
# 2. Bar
# 3. Baz
#
# Instead of:
# 1. Foo
# 1. Bar
# 1. Baz
rule 'MD029', :style => :ordered
# MD033 Inline HTML
# TODO: Tweak elements when https://github.com/markdownlint/markdownlint/issues/118 will be done?
exclude_rule 'MD033'
# MD034 Bare URL used
# TODO: Remove when https://github.com/markdownlint/markdownlint/issues/328 will be fixed.
exclude_rule 'MD034'
# MD041 First line in file should be a top level header
# Because we use HTML to hide them on the website.
exclude_rule 'MD041'

46
docs/packaging/README.md Normal file
View File

@ -0,0 +1,46 @@
# HTTPie release process
Welcome on the documentation part of the **HTTPie release process**.
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
- If you are looking for HTTPie installation or upgrade instructions, then you can find all you need for your OS on [that page](https://httpie.io/docs#installation). In the case you do not find your OS, [let us know](https://github.com/httpie/httpie/issues/).
- If you are looking for technical information about the HTTPie packaging, then you are at the good place.
## About
You are looking at the HTTPie packaging documentation, where you will find valuable information about how we manage to release HTTPie to lots of OSes, including technical data that may be worth reading if you are a package maintainer.
The overall release process starts simple:
1. Do the [PyPI](https://pypi.org/project/httpie/) publication.
2. Then, handle company-related tasks.
3. Finally, follow OS-specific steps, described in documents below, to send patches downstream.
## First, PyPI
Let's do the release on [PyPi](https://pypi.org/project/httpie/).
That is done quite easily by manually triggering the [release workflow](https://github.com/httpie/httpie/actions/workflows/release.yml).
## Then, company-specific tasks
- Blank the `master_and_released_docs_differ_after` value in [config.json](https://github.com/httpie/httpie/blob/master/docs/config.json).
- Update the [contributors list](../contributors).
- Update the HTTPie version bundled into [Termible](https://termible.io/) ([example](https://github.com/httpie/termible/pull/1)).
## Finally, spread dowstream
Find out how we do release new versions for each and every supported OS in the following table.
A more complete state of deployment can be found on [repology](https://repology.org/project/httpie/versions), including unofficial packages.
| OS | Maintainer |
| -------------------------------------------: | -------------- |
| [Arch Linux, and derived](linux-arch/) | trusted person |
| [CentOS, RHEL, and derived](linux-centos/) | trusted person |
| [Debian, Ubuntu, and derived](linux-debian/) | trusted person |
| [Fedora](linux-fedora/) | trusted person |
| :construction: [Homebrew, Linuxbrew](brew/) | **HTTPie** |
| :construction: [MacPorts](mac-ports/) | **HTTPie** |
| [Snapcraft](snapcraft/) | **HTTPie** |
| [Windows — Chocolatey](windows-chocolatey/) | **HTTPie** |
:new: You do not find your system or you would like to see HTTPie supported on another OS? Then [let us know](https://github.com/httpie/httpie/issues/).

View File

@ -0,0 +1,33 @@
# HTTPie on Homebrew, and Linuxbrew
Welcome to the documentation about **packaging HTTPie for Homebrew**.
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
- If you are looking for HTTPie installation or upgrade instructions on Homebrew, then you can find them on [that page](https://httpie.io/docs#homebrew) ([that one](https://httpie.io/docs#linuxbrew) for Linuxbrew).
- If you are looking for technical information about the HTTPie packaging on Homebrew, then you are in a good place.
## About
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Homebrew. They apply to Linuxbrew as well.
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
## Overall process
:construction: Work in progress.
First, update the current Formula:
```bash
make brew-deps
# Copy-paste content into downstream/mac/brew/httpie.rb
git add downstream/mac/brew/httpie.rb
git commit -s -m 'Update brew formula to XXX'
```
That [GitHub workflow](https://github.com/httpie/httpie/actions/workflows/test-package-mac-brew.yml) will test the formula when `downstream/mac/brew/httpie.rb` is changed in a pull request.
Then, open a pull request with those changes to the [downstream file](https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb).
## Hacking
:construction: Work in progress.

View File

@ -15,21 +15,23 @@ import requests
VERSIONS = {
# By default, we use the latest packages. But sometimes Requests has a maximum supported versions.
# Take a look here before making a release: <https://github.com/psf/requests/blob/master/setup.py>
'idna': '2.10',
'idna': '3.2',
}
# Note: Keep that list sorted.
PACKAGES = [
'certifi',
'charset-normalizer',
'defusedxml',
'httpie',
'idna',
'Pygments',
'PySocks',
'requests',
'requests-toolbelt',
'certifi',
'urllib3',
'idna',
'chardet',
'PySocks',
'defusedxml',
'multidict',
]

View File

@ -0,0 +1,86 @@
class Httpie < Formula
include Language::Python::Virtualenv
desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.io/"
url "https://files.pythonhosted.org/packages/53/96/cbcfec73c186f076e4443faf3d91cbbc868f18f6323703afd348b1aba46d/httpie-2.6.0.tar.gz"
sha256 "ef929317b239bbf0a5bb7159b4c5d2edbfc55f8a0bcf9cd24ce597daec2afca5"
license "BSD-3-Clause"
head "https://github.com/httpie/httpie.git", branch: "master"
bottle do
sha256 cellar: :any_skip_relocation, arm64_monterey: "83aab05ffbcd4c3baa6de6158d57ebdaa67c148bef8c872527d90bdaebff0504"
sha256 cellar: :any_skip_relocation, arm64_big_sur: "3c3a5c2458d0658e14b663495e115297c573aa3466d292f12d02c3ec13a24bdf"
sha256 cellar: :any_skip_relocation, monterey: "f860e7d3b77dca4928a2c5e10c4cbd50d792330dfb99f7d736ca0da9fb9dd0d0"
sha256 cellar: :any_skip_relocation, big_sur: "377b0643aa1f6d310ba4cfc70d66a94cc458213db8d134940d3b10a32defacf1"
sha256 cellar: :any_skip_relocation, catalina: "6d306c30f6f1d7a551d88415efe12b7c3f25d0602f3579dc632771a463f78fa5"
sha256 cellar: :any_skip_relocation, mojave: "f66b8cdff9cb7b44a84197c3e3d81d810f7ff8f2188998b977ccadfc7e2ec893"
sha256 cellar: :any_skip_relocation, x86_64_linux: "53f036b0114814c28982e8c022dcf494e7024de088641d7076fd73d12a45a0e9"
end
depends_on "python@3.10"
resource "certifi" do
url "https://files.pythonhosted.org/packages/6c/ae/d26450834f0acc9e3d1f74508da6df1551ceab6c2ce0766a593362d6d57f/certifi-2021.10.8.tar.gz"
sha256 "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"
end
resource "charset-normalizer" do
url "https://files.pythonhosted.org/packages/48/44/76b179e0d1afe6e6a91fd5661c284f60238987f3b42b676d141d01cd5b97/charset-normalizer-2.0.10.tar.gz"
sha256 "876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"
end
resource "defusedxml" do
url "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz"
sha256 "1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"
end
resource "idna" do
url "https://files.pythonhosted.org/packages/cb/38/4c4d00ddfa48abe616d7e572e02a04273603db446975ab46bbcd36552005/idna-3.2.tar.gz"
sha256 "467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
end
resource "Pygments" do
url "https://files.pythonhosted.org/packages/94/9c/cb656d06950268155f46d4f6ce25d7ffc51a0da47eadf1b164bbf23b718b/Pygments-2.11.2.tar.gz"
sha256 "4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
end
resource "PySocks" do
url "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz"
sha256 "3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
end
resource "requests" do
url "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz"
sha256 "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"
end
resource "requests-toolbelt" do
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
end
resource "urllib3" do
url "https://files.pythonhosted.org/packages/b0/b1/7bbf5181f8e3258efae31702f5eab87d8a74a72a0aa78bc8c08c1466e243/urllib3-1.26.8.tar.gz"
sha256 "0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
end
resource "multidict" do
url "https://files.pythonhosted.org/packages/8e/7c/e12a69795b7b7d5071614af2c691c97fbf16a2a513c66ec52dd7d0a115bb/multidict-5.2.0.tar.gz"
sha256 "0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"
end
def install
virtualenv_install_with_resources
end
test do
# shell_output() already checks the status code
shell_output("#{bin}/httpie -v")
shell_output("#{bin}/https -v")
shell_output("#{bin}/http -v")
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
end
end

View File

@ -0,0 +1,47 @@
# Maintainer: Jelle van der Waa <jelle@archlinux.org>
# Maintainer: daurnimator <daurnimator@archlinux.org>
# Contributor: Daniel Micay <danielmicay@gmail.com>
# Contributor: Thomas Weißschuh <thomas_weissschuh lavabit com>
pkgname=httpie
pkgver=2.6.0
pkgrel=1
pkgdesc="human-friendly CLI HTTP client for the API era"
url="https://github.com/httpie/httpie"
depends=('python-defusedxml'
'python-pygments'
'python-pysocks'
'python-requests'
'python-requests-toolbelt'
'python-charset-normalizer')
makedepends=('python-setuptools')
checkdepends=('python-pytest'
'python-pytest-httpbin'
'python-responses')
conflicts=(python-httpie)
replaces=(python-httpie python2-httpie)
license=('BSD')
arch=('any')
source=($pkgname-$pkgver.tar.gz::"https://github.com/httpie/httpie/archive/$pkgver.tar.gz")
sha256sums=('3bcd9a8cb2b11299da12d3af36c095c6d4b665e41c395898a07f1ae4d99fc14a')
build() {
cd $pkgname-$pkgver
python3 setup.py build
}
package() {
cd $pkgname-$pkgver
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/httpie/LICENSE"
python3 setup.py install --root="$pkgdir" --optimize=1
# Fix upstream, include them in MANIFEST.in and use data_files in setup.py to install them automatically
# TODO: add zsh support
install -Dm644 extras/httpie-completion.bash "$pkgdir"/usr/share/bash-completion/completions/http
install -Dm644 extras/httpie-completion.fish "$pkgdir"/usr/share/fish/vendor_completions.d/http.fish
}
check() {
cd $pkgname-$pkgver
PYTHONDONTWRITEBYTECODE=1 pytest tests
}

View 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.

View 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.

View File

@ -0,0 +1,29 @@
# HTTPie on Debian, Ubuntu, and derived
Welcome to the documentation about **packaging HTTPie for Debian GNU/Linux**.
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
- If you are looking for HTTPie installation or upgrade instructions on Debian GNU/Linux, then you can find them on [that page](https://httpie.io/docs#debian-and-ubuntu).
- If you are looking for technical information about the HTTPie packaging on Debian GNU/Linux, then you are in a good place.
## About
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Debian GNU/Linux. They apply to Ubuntu as well, and any Debian-derived distributions like MX Linux, Linux Mint, deepin, Pop!_OS, KDE neon, Zorin OS, elementary OS, Kubuntu, Devuan, Linux Lite, Peppermint OS, Lubuntu, antiX, Xubuntu, etc.
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
The current maintainer is Bartosz Fenski.
## Overall process
Open a new bug on the Debian Bug Tracking System by sending an email:
- To: `Debian Bug Tracking System <submit@bugs.debian.org>`
- Subject: `httpie: Version XXX available`
- Message template (examples [1](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=993937), and [2](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=996479)):
```email
Package: httpie
Severity: normal
<MESSAGE>
```

View File

@ -0,0 +1,48 @@
# HTTPie on Fedora
Welcome to the documentation about **packaging HTTPie for Fedora**.
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
- If you are looking for HTTPie installation or upgrade instructions on Fedora, then you can find them on [that page](https://httpie.io/docs#fedora).
- If you are looking for technical information about the HTTPie packaging on Fedora, then you are in a good place.
## About
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for Fedora.
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
The current maintainer is [Miro Hrončok](https://github.com/hroncok).
## Overall process
We added the [.packit.yaml](https://github.com/httpie/httpie/blob/master/.packit.yaml) local file.
It unlocks real-time Fedora checks on pull requests and new releases.
So there is nothing to do on our side: `Packit` will see the new release and open a pull request [there](https://src.fedoraproject.org/rpms/httpie). Then, the Fedora maintainer will review and merge.
It is also possible to follow [user feedbacks](https://bodhi.fedoraproject.org/updates/?packages=httpie) for all builds.
## Q/A with Miro
Q: What would the command to install the latest stable version look like?
A: Assuming the latest stable version is already propagated to Fedora:
```bash
# Note that yum is an alias to dnf.
$ sudo dnf install httpie
```
Q: Will dnf/yum upgrade then update to the latest?
A: Yes, assuming the same as above.
Q: Are new versions backported automatically?
A: No. The process is:
1. A new HTTPie release is created on Github.
2. A pull request for Fedora `rawhide` (the development version of Fedora, currently Fedora 36) is created.
3. A Fedora packager (usually Miro) sanity checks the pull request and merges, builds. HTTPie is updated in `rawhide` within 24 hours (sometimes more, for unrelated issues).
4. A Fedora packager decides whether the upgrade is suitable for stable Fedora releases (currently 35, 34, 33), if so, merges the changes there.
5. (if the above is yes) The new version of HTTPie lands in `updates-testing` repo where it waits for user feedback and lands within ~1 week for broad availability.

View File

@ -0,0 +1,251 @@
Name: httpie
Version: 2.6.0
Release: 1%{?dist}
Summary: A Curl-like tool for humans
License: BSD
URL: https://httpie.org/
Source0: https://github.com/httpie/httpie/archive/%{version}/%{name}-%{version}.tar.gz
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
BuildRequires: help2man
%description
HTTPie is a CLI HTTP utility built out of frustration with existing tools. The
goal is to make CLI interaction with HTTP-based services as human-friendly as
possible.
HTTPie does so by providing an http command that allows for issuing arbitrary
HTTP requests using a simple and natural syntax and displaying colorized
responses.
%prep
%autosetup -p1
%generate_buildrequires
%pyproject_buildrequires -rx test
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files httpie
# Bash completion
mkdir -p %{buildroot}%{_datadir}/bash-completion/completions
cp -a extras/httpie-completion.bash %{buildroot}%{_datadir}/bash-completion/completions/http
ln -s ./http %{buildroot}%{_datadir}/bash-completion/completions/https
# Fish completion
mkdir -p %{buildroot}%{_datadir}/fish/vendor_completions.d/
cp -a extras/httpie-completion.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/http.fish
ln -s ./http.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/https.fish
# Generate man pages for everything
export PYTHONPATH=%{buildroot}%{python3_sitelib}
mkdir -p %{buildroot}%{_mandir}/man1
help2man %{buildroot}%{_bindir}/http > %{buildroot}%{_mandir}/man1/http.1
help2man %{buildroot}%{_bindir}/https > %{buildroot}%{_mandir}/man1/https.1
help2man %{buildroot}%{_bindir}/httpie > %{buildroot}%{_mandir}/man1/httpie.1
%check
%pytest -v
%files -f %{pyproject_files}
%doc README.md
%license LICENSE
%{_bindir}/http
%{_bindir}/https
%{_bindir}/httpie
%{_mandir}/man1/http.1*
%{_mandir}/man1/https.1*
%{_mandir}/man1/httpie.1*
# we co-own the entire directory structures for bash/fish completion to avoid a dependency
%{_datadir}/bash-completion/
%{_datadir}/fish/
%changelog
* Fri Oct 15 2021 Miro Hrončok <mhroncok@redhat.com> - 2.6.0-1
- Update to 2.6.0
- Fixes: rhbz#2014022
* Tue Sep 07 2021 Miro Hrončok <mhroncok@redhat.com> - 2.5.0-1
- Update to 2.5.0
- Fixes: rhbz#2001693
* Thu Jul 22 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2.4.0-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
* Fri Jun 04 2021 Python Maint <python-maint@redhat.com> - 2.4.0-3
- Rebuilt for Python 3.10
* Thu May 27 2021 Miro Hrončok <mhroncok@redhat.com> - 2.4.0-2
- Add Bash and Fish completion
- Fixes rhbz#1834441
- Run tests on build time
* Wed Mar 24 2021 Mikel Olasagasti Uranga <mikel@olasagasti.info> - 2.4.0-1
- Update to 2.4.0
- Use pypi_source macro
* Tue Jan 26 2021 Fedora Release Engineering <releng@fedoraproject.org> - 2.3.0-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
* Thu Jan 21 2021 Nils Philippsen <nils@tiptoe.de> - 2.3.0-2
- use macros for Python dependencies
- add missing Python dependencies needed for running help2man
- remove manual Python dependencies
- discard stderr when running help2man
* Thu Dec 24 2020 Nils Philippsen <nils@tiptoe.de> - 2.3.0-1
- version 2.3.0
- Python 2 is no more
- use %%autosetup and Python build macros
- remove EL7-isms
- explicitly require sed for building
* Tue Jul 28 2020 Fedora Release Engineering <releng@fedoraproject.org> - 1.0.3-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild
* Tue May 26 2020 Miro Hrončok <mhroncok@redhat.com> - 1.0.3-3
- Rebuilt for Python 3.9
* Wed Jan 29 2020 Fedora Release Engineering <releng@fedoraproject.org> - 1.0.3-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
* Mon Sep 30 2019 Rick Elrod <relrod@redhat.com> - 1.0.3-1
- Latest upstream
* Mon Aug 19 2019 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-15
- Rebuilt for Python 3.8
* Thu Jul 25 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-14
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
* Fri Feb 01 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-13
- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
* Fri Jul 13 2018 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-12
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
* Tue Jun 19 2018 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-11
- Rebuilt for Python 3.7
* Wed Feb 07 2018 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-10
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
* Wed Jul 26 2017 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-9
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
* Fri Mar 10 2017 Ralph Bean <rbean@redhat.com> - 0.9.4-8
- Fix help2man usage with python3.
https://bugzilla.redhat.com/show_bug.cgi?id=1430733
* Mon Feb 27 2017 Ralph Bean <rbean@redhat.com> - 0.9.4-7
- Fix missing Requires. https://bugzilla.redhat.com/show_bug.cgi?id=1417730
* Fri Feb 10 2017 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.4-6
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
* Mon Jan 2 2017 Ricky Elrod <relrod@redhat.com> - 0.9.4-5
- Add missing Obsoletes.
* Mon Jan 2 2017 Ricky Elrod <relrod@redhat.com> - 0.9.4-4
- Nuke python-version-specific subpackages. Just use py3 if we can.
* Mon Dec 19 2016 Miro Hrončok <mhroncok@redhat.com> - 0.9.4-3
- Rebuild for Python 3.6
* Tue Jul 19 2016 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.4-2
- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages
* Tue Jul 05 2016 Ricky Elrod <relrod@redhat.com> - 0.9.4-1
- Update to latest upstream.
* Fri Jun 03 2016 Ricky Elrod <relrod@redhat.com> - 0.9.3-4
- Add proper Obsoletes for rhbz#1329226.
* Wed Feb 03 2016 Fedora Release Engineering <releng@fedoraproject.org> - 0.9.3-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild
* Mon Jan 04 2016 Ralph Bean <rbean@redhat.com> - 0.9.3-2
- Modernize python macros and subpackaging.
- Move LICENSE to %%license macro.
- Make python3 the default on modern Fedora.
* Mon Jan 04 2016 Ralph Bean <rbean@redhat.com> - 0.9.3-1
- new version
* Tue Nov 10 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.2-3
- Rebuilt for https://fedoraproject.org/wiki/Changes/python3.5
* Wed Jun 17 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.2-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
* Thu Mar 26 2015 Ricky Elrod <relrod@redhat.com> - 0.9.2-1
- Latest upstream release.
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.8.0-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
* Wed May 28 2014 Kalev Lember <kalevlember@gmail.com> - 0.8.0-2
- Rebuilt for https://fedoraproject.org/wiki/Changes/Python_3.4
* Fri Jan 31 2014 Ricky Elrod <codeblock@fedoraproject.org> - 0.8.0-1
- Latest upstream release.
* Fri Oct 4 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.7.2-2
- Add in patch to work without having python-requests 2.0.0.
* Sat Sep 28 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.7.2-1
- Latest upstream release.
* Thu Sep 5 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-7
- Only try building the manpage on Fedora, since RHEL's help2man doesn't
have the --no-discard-stderr flag.
* Thu Sep 5 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-6
- Loosen the requirement on python-pygments.
* Sat Aug 03 2013 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.6.0-5
- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild
* Tue Jul 2 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-4
- python-requests 1.2.3 exists in rawhide now.
* Sun Jun 30 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-3
- Patch to use python-requests 1.1.0 for now.
* Sat Jun 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.6.0-2
- Update to latest upstream release.
* Mon Apr 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.5.0-2
- Fix changelog messup.
* Mon Apr 29 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.5.0-1
- Update to latest upstream release.
* Mon Apr 8 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-3
- Fix manpage generation by exporting PYTHONPATH.
* Tue Mar 26 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-2
- Include Python3 support, and fix other review blockers.
* Mon Mar 11 2013 Ricky Elrod <codeblock@fedoraproject.org> - 0.4.1-1
- Update to latest upstream release
* Thu Jul 19 2012 Ricky Elrod <codeblock@fedoraproject.org> - 0.2.5-1
- Initial build.

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8::et:sw=4:ts=4:sts=4
PortSystem 1.0
PortGroup github 1.0
PortGroup python 1.0
github.setup httpie httpie 2.6.0
maintainers {g5pw @g5pw} openmaintainer
categories net
description Modern, user-friendly command-line HTTP client for the API era
long_description HTTPie (pronounced aych-tee-tee-pie) is a command line HTTP \
client. Its goal is to make CLI interaction with web \
services as human-friendly as possible. It provides a simple \
http command that allows for sending arbitrary HTTP requests \
using a simple and natural syntax, and displays colorized \
responses. HTTPie can be used for testing, debugging, and \
generally interacting with HTTP servers.
platforms darwin
license BSD
homepage https://httpie.io/
variant python37 conflicts python36 python38 python39 python310 description "Use Python 3.7" {}
variant python38 conflicts python36 python37 python39 python310 description "Use Python 3.8" {}
variant python39 conflicts python36 python37 python38 python310 description "Use Python 3.9" {}
variant python310 conflicts python36 python37 python38 python39 description "Use Python 3.10" {}
if {[variant_isset python37]} {
python.default_version 37
} elseif {[variant_isset python39]} {
python.default_version 39
} elseif {[variant_isset python310]} {
python.default_version 310
} else {
default_variants +python38
python.default_version 38
}
depends_lib-append port:py${python.version}-requests \
port:py${python.version}-requests-toolbelt \
port:py${python.version}-pygments \
port:py${python.version}-socks \
port:py${python.version}-charset-normalizer \
port:py${python.version}-defusedxml
checksums rmd160 07b1d1592da1c505ed3ee4ef3b6056215e16e9ff \
sha256 63cf104bf3552305c68a74f16494a90172b15296610a875e17918e5e36373c0b \
size 1133491
python.link_binaries_suffix

View File

@ -0,0 +1,40 @@
# HTTPie on MacPorts
Welcome to the documentation about **packaging HTTPie for MacPorts**.
- If you do not know HTTPie, have a look [here](https://httpie.io/cli).
- If you are looking for HTTPie installation or upgrade instructions on MacPorts, then you can find them on [that page](https://httpie.io/docs#macports).
- If you are looking for technical information about the HTTPie packaging on MacPorts, then you are in a good place.
## About
This document contains technical details, where we describe how to create a patch for the latest HTTPie version for MacPorts.
We will discuss setting up the environment, installing development tools, installing and testing changes before submitting a patch downstream.
## Overall process
Open a pull request to update the [downstream file](https://github.com/macports/macports-ports/blob/master/net/httpie/Portfile) ([example](https://github.com/macports/macports-ports/pull/12583)).
- Here is how to calculate the size and checksums (replace `2.5.0` with the correct version):
```bash
# Download the archive
$ wget https://api.github.com/repos/httpie/httpie/tarball/2.5.0
# Size
$ stat --printf="%s\n" 2.5.0
1105185
# Checksums
$ openssl dgst -rmd160 2.5.0
RIPEMD160(2.5.0)= 88d227d52199c232c0ddf704a219d1781b1e77ee
$ openssl dgst -sha256 2.5.0
SHA256(2.5.0)= 00c4b7bbe7f65abe1473f37b39d9d9f8f53f44069a430ad143a404c01c2179fc
```
- The commit message must be `httpie: update to XXX`.
- The commit must be signed-off (`git commit -s`).
## Hacking
:construction: Work in progress.

View 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
```

View 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
```

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
<metadata>
<id>httpie</id>
<version>2.6.0</version>
<summary>Modern, user-friendly command-line HTTP client for the API era</summary>
<description>
HTTPie *aitch-tee-tee-pie* is a user-friendly command-line HTTP client for the API era.
It comes with JSON support, syntax highlighting, persistent sessions, wget-like downloads, plugins, and more.
The project's goal is to make CLI interaction with web services as human-friendly as possible. HTTPie is designed for testing, debugging, and generally interacting with APIs and HTTP servers.
The `http` and `https` commands allow for creating and sending arbitrary HTTP requests. They use simple and natural syntax and provide formatted and colorized output.
Main features:
- Built-in JSON support
- Colorized and formatted terminal output
- Sensible defaults for the API era
- Persistent sessions
- Forms and file uploads
- HTTPS, proxies, and authentication support
- Support for arbitrary request data and headers
- Wget-like downloads
- Extensions API
- Expressive and intuitive syntax
- Linux, macOS, Windows, and FreeBSD support
- All that and more in 2 simple commands: `http` + `https`
</description>
<title>HTTPie</title>
<authors>HTTPie</authors>
<owners>jakubroztocil</owners>
<copyright>2012-2021 Jakub Roztocil</copyright>
<licenseUrl>https://raw.githubusercontent.com/httpie/httpie/master/LICENSE</licenseUrl>
<iconUrl>https://pie-assets.s3.eu-central-1.amazonaws.com/LogoIcons/GB.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<releaseNotes>See the [changelog](https://github.com/httpie/httpie/blob/2.6.0/CHANGELOG.md).</releaseNotes>
<tags>httpie http https rest api client curl python ssl cli foss oss url</tags>
<projectUrl>https://httpie.io</projectUrl>
<packageSourceUrl>https://github.com/httpie/httpie/tree/master/docs/packaging/windows-chocolatey</packageSourceUrl>
<projectSourceUrl>https://github.com/httpie/httpie</projectSourceUrl>
<docsUrl>https://httpie.io/docs</docsUrl>
<bugTrackerUrl>https://github.com/httpie/httpie/issues</bugTrackerUrl>
<dependencies>
<dependency id="python3" version="3.7" />
</dependencies>
</metadata>
<files>
<file src="tools\**" target="tools" />
</files>
</package>

View File

@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop';
py -m pip install $env:ChocolateyPackageName==$env:ChocolateyPackageVersion --disable-pip-version-check

View File

@ -0,0 +1,2 @@
$ErrorActionPreference = 'Stop';
py -m pip uninstall -y $env:ChocolateyPackageName --disable-pip-version-check

View File

@ -7,7 +7,7 @@ _http_complete() {
fi
}
complete -o default -F _http_complete http
complete -o default -F _http_complete http httpie.http httpie.https https
_http_complete_options() {
local cur_word=$1

View File

@ -1,76 +0,0 @@
# The latest Homebrew formula as submitted to Homebrew/homebrew-core.
# Only useful for testing until it gets accepted by homebrew maintainers.
# (It will need to be updated from the repo version before next release.)
#
# https://github.com/Homebrew/homebrew-core/blob/master/Formula/httpie.rb
#
class Httpie < Formula
include Language::Python::Virtualenv
desc "User-friendly cURL replacement (command-line HTTP client)"
homepage "https://httpie.io/"
url "https://files.pythonhosted.org/packages/17/3a/90fb6702e600f5ba7d38d147bbc0b0a1e47159e3e244737319c98c140420/httpie-2.4.0.tar.gz"
sha256 "4d1bf5779cf6c9007351cfcaa20bd19947267dc026af09246db6006a8927d8c6"
license "BSD-3-Clause"
head "https://github.com/httpie/httpie.git"
bottle do
rebuild 1
sha256 cellar: :any_skip_relocation, arm64_big_sur: "a01ce8767f6ea88eb8e7894347ba64eb29294053a8ee91eed44dfaf0ab5e7ea2"
sha256 cellar: :any_skip_relocation, big_sur: "bdffeff349595ed3c528ed791d568e308b0877246b49e05e867143ba3415a70f"
sha256 cellar: :any_skip_relocation, catalina: "ba0627d70f0ee49c64677f5554881ebd56371f47d45196b6564680089ce69152"
sha256 cellar: :any_skip_relocation, mojave: "0b87901e88bdcf53c55c5138677087b4621c5aaf1fca67b53b730d5a2fd5a40a"
sha256 cellar: :any_skip_relocation, high_sierra: "87e7348b6fb40fd8e4f7597937952469601962189e62d321b8cb4fa421e035ef"
end
depends_on "python@3.9"
resource "Pygments" do
url "https://files.pythonhosted.org/packages/e1/86/8059180e8217299079d8719c6e23d674aadaba0b1939e25e0cc15dcf075b/Pygments-2.7.4.tar.gz"
sha256 "df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"
end
resource "requests" do
url "https://files.pythonhosted.org/packages/6b/47/c14abc08432ab22dc18b9892252efaf005ab44066de871e72a38d6af464b/requests-2.25.1.tar.gz"
sha256 "27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"
end
resource "requests-toolbelt" do
url "https://files.pythonhosted.org/packages/28/30/7bf7e5071081f761766d46820e52f4b16c8a08fef02d2eb4682ca7534310/requests-toolbelt-0.9.1.tar.gz"
sha256 "968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
end
resource "certifi" do
url "https://files.pythonhosted.org/packages/06/a9/cd1fd8ee13f73a4d4f491ee219deeeae20afefa914dfb4c130cfc9dc397a/certifi-2020.12.5.tar.gz"
sha256 "1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"
end
resource "urllib3" do
url "https://files.pythonhosted.org/packages/d7/8d/7ee68c6b48e1ec8d41198f694ecdc15f7596356f2ff8e6b1420300cf5db3/urllib3-1.26.3.tar.gz"
sha256 "de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"
end
resource "idna" do
url "https://files.pythonhosted.org/packages/ea/b7/e0e3c1c467636186c39925827be42f16fee389dc404ac29e930e9136be70/idna-2.10.tar.gz"
sha256 "b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"
end
resource "chardet" do
url "https://files.pythonhosted.org/packages/ee/2d/9cdc2b527e127b4c9db64b86647d567985940ac3698eeabc7ffaccb4ea61/chardet-4.0.0.tar.gz"
sha256 "0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"
end
resource "PySocks" do
url "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz"
sha256 "3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
end
def install
virtualenv_install_with_resources
end
test do
raw_url = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD/Formula/httpie.rb"
assert_match "PYTHONPATH", shell_output("#{bin}/http --ignore-stdin #{raw_url}")
end
end

View File

@ -0,0 +1,202 @@
"""
This file is the declaration of benchmarks for HTTPie. It
is also used to run them with the current environment.
Each instance of BaseRunner class will be an individual
benchmark. And if run without any arguments, this file
will execute every benchmark instance and report the
timings.
The benchmarks are run through 'pyperf', which allows to
do get very precise results. For micro-benchmarks like startup,
please run `pyperf system tune` to get even more acurrate results.
Examples:
# Run everything as usual, the default is that we do 3 warmup runs
# and 5 actual runs.
$ python extras/profiling/benchmarks.py
# For retrieving results faster, pass --fast
$ python extras/profiling/benchmarks.py --fast
# For verify everything works as expected, pass --debug-single-value.
# It will only run everything once, so the resuls are not realiable. But
# very useful when iterating on a benchmark
$ python extras/profiling/benchmarks.py --debug-single-value
# If you want to run with a custom HTTPie command (for example with
# and HTTPie instance installed in another virtual environment),
# pass HTTPIE_COMMAND variable.
$ HTTPIE_COMMAND="/my/python /my/httpie" python extras/profiling/benchmarks.py
"""
from __future__ import annotations
import os
import shlex
import subprocess
import sys
import threading
from contextlib import ExitStack, contextmanager
from dataclasses import dataclass, field
from functools import cached_property, partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
from tempfile import TemporaryDirectory
from typing import ClassVar, Final, List
import pyperf
# For download benchmarks, define a set of files.
# file: (block_size, count) => total_size = block_size * count
PREDEFINED_FILES: Final = {'3G': (3 * 1024 ** 2, 1024)}
class QuietSimpleHTTPServer(SimpleHTTPRequestHandler):
def log_message(self, *args, **kwargs):
pass
@contextmanager
def start_server():
"""Create a server to serve local files. It will create the
PREDEFINED_FILES through dd."""
with TemporaryDirectory() as directory:
for file_name, (block_size, count) in PREDEFINED_FILES.items():
subprocess.check_call(
[
'dd',
'if=/dev/zero',
f'of={file_name}',
f'bs={block_size}',
f'count={count}',
],
cwd=directory,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
handler = partial(QuietSimpleHTTPServer, directory=directory)
server = HTTPServer(('localhost', 0), handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
yield '{}:{}'.format(*server.socket.getsockname())
server.shutdown()
thread.join(timeout=0.5)
@dataclass
class Context:
benchmarks: ClassVar[List[BaseRunner]] = []
stack: ExitStack = field(default_factory=ExitStack)
runner: pyperf.Runner = field(default_factory=pyperf.Runner)
def run(self) -> pyperf.BenchmarkSuite:
results = [benchmark.run(self) for benchmark in self.benchmarks]
return pyperf.BenchmarkSuite(results)
@property
def cmd(self) -> List[str]:
if cmd := os.getenv('HTTPIE_COMMAND'):
return shlex.split(cmd)
http = os.path.join(os.path.dirname(sys.executable), 'http')
assert os.path.exists(http)
return [sys.executable, http]
@cached_property
def server(self) -> str:
return self.stack.enter_context(start_server())
def __enter__(self):
return self
def __exit__(self, *exc_info):
self.stack.close()
@dataclass
class BaseRunner:
"""
An individual benchmark case. By default it has the category
(e.g like startup or download) and a name.
"""
category: str
title: str
def __post_init__(self):
Context.benchmarks.append(self)
def run(self, context: Context) -> pyperf.Benchmark:
raise NotImplementedError
@property
def name(self) -> str:
return f'{self.title} ({self.category})'
@dataclass
class CommandRunner(BaseRunner):
"""
Run a single command, and benchmark it.
"""
args: List[str]
def run(self, context: Context) -> pyperf.Benchmark:
return context.runner.bench_command(self.name, [*context.cmd, *self.args])
@dataclass
class DownloadRunner(BaseRunner):
"""
Benchmark downloading a single file from the
remote server.
"""
file_name: str
def run(self, context: Context) -> pyperf.Benchmark:
return context.runner.bench_command(
self.name,
[
*context.cmd,
'--download',
'GET',
f'{context.server}/{self.file_name}',
],
)
CommandRunner('startup', '`http --version`', ['--version'])
CommandRunner('startup', '`http --offline pie.dev/get`', ['--offline', 'pie.dev/get'])
for pretty in ['all', 'none']:
CommandRunner(
'startup',
f'`http --pretty={pretty} pie.dev/stream/1000`',
[
'--print=HBhb',
f'--pretty={pretty}',
'httpbin.org/stream/1000'
]
)
DownloadRunner('download', '`http --download :/big_file.txt` (3GB)', '3G')
def main() -> None:
# PyPerf will bring it's own argument parser, so configure the script.
# The somewhat fast and also precise enough configuration is this. We run
# benchmarks 3 times to warmup (e.g especially for download benchmark, this
# is important). And then 5 actual runs where we record.
sys.argv.extend(
['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2']
)
with Context() as context:
context.run()
if __name__ == '__main__':
main()

287
extras/profiling/run.py Normal file
View File

@ -0,0 +1,287 @@
"""
Run the HTTPie benchmark suite with multiple environments.
This script is configured in a way that, it will create
two (or more) isolated environments and compare the *last
commit* of this repository with it's master.
> If you didn't commit yet, it won't be showing results.
You can also pass --fresh, which would test the *last
commit* of this repository with a fresh copy of HTTPie
itself. This way even if you don't have an up-to-date
master branch, you can still compare it with the upstream's
master.
You can also pass --complex to add 2 additional environments,
which would include additional dependencies like pyOpenSSL.
Examples:
# Run everything as usual, and compare last commit with master
$ python extras/benchmarks/run.py
# Include complex environments
$ python extras/benchmarks/run.py --complex
# Compare against a fresh copy
$ python extras/benchmarks/run.py --fresh
# Compare against a custom branch of a custom repo
$ python extras/benchmarks/run.py --target-repo my_repo --target-branch my_branch
# Debug changes made on this script (only run benchmarks once)
$ python extras/benchmarks/run.py --debug
"""
import dataclasses
import shlex
import subprocess
import sys
import tempfile
import venv
from argparse import ArgumentParser, FileType
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import (IO, Dict, Generator, Iterable, List, Optional,
Tuple)
BENCHMARK_SCRIPT = Path(__file__).parent / 'benchmarks.py'
CURRENT_REPO = Path(__file__).parent.parent.parent
GITHUB_URL = 'https://github.com/httpie/httpie.git'
TARGET_BRANCH = 'master'
# Additional dependencies for --complex
ADDITIONAL_DEPS = ('pyOpenSSL',)
def call(*args, **kwargs):
kwargs.setdefault('stdout', subprocess.DEVNULL)
return subprocess.check_call(*args, **kwargs)
class Environment:
"""
Each environment defines how to create an isolated instance
where we could install HTTPie and run benchmarks without any
environmental factors.
"""
@contextmanager
def on_repo(self) -> Generator[Tuple[Path, Dict[str, str]], None, None]:
"""
Return the path to the python interpreter and the
environment variables (e.g HTTPIE_COMMAND) to be
used on the benchmarks.
"""
raise NotImplementedError
@dataclass
class HTTPieEnvironment(Environment):
repo_url: str
branch: Optional[str] = None
dependencies: Iterable[str] = ()
@contextmanager
def on_repo(self) -> Generator[Path, None, None]:
with tempfile.TemporaryDirectory() as directory_path:
directory = Path(directory_path)
# Clone the repo
repo_path = directory / 'httpie'
call(
['git', 'clone', self.repo_url, repo_path],
stderr=subprocess.DEVNULL,
)
if self.branch is not None:
call(
['git', 'checkout', self.branch],
cwd=repo_path,
stderr=subprocess.DEVNULL,
)
# Prepare the environment
venv_path = directory / '.venv'
venv.create(venv_path, with_pip=True)
# Install basic dependencies
python = venv_path / 'bin' / 'python'
call(
[
python,
'-m',
'pip',
'install',
'wheel',
'pyperf==2.3.0',
*self.dependencies,
]
)
# Create a wheel distribution of HTTPie
call([python, 'setup.py', 'bdist_wheel'], cwd=repo_path)
# Install httpie
distribution_path = next((repo_path / 'dist').iterdir())
call(
[python, '-m', 'pip', 'install', distribution_path],
cwd=repo_path,
)
http = venv_path / 'bin' / 'http'
yield python, {'HTTPIE_COMMAND': shlex.join([str(python), str(http)])}
@dataclass
class LocalCommandEnvironment(Environment):
local_command: str
@contextmanager
def on_repo(self) -> Generator[Path, None, None]:
yield sys.executable, {'HTTPIE_COMMAND': self.local_command}
def dump_results(
results: List[str],
file: IO[str],
min_speed: Optional[str] = None
) -> None:
for result in results:
lines = result.strip().splitlines()
if min_speed is not None and "hidden" in lines[-1]:
lines[-1] = (
'Some benchmarks were hidden from this list '
'because their timings did not change in a '
'significant way (change was within the error '
'margin ±{margin}%).'
).format(margin=min_speed)
result = '\n'.join(lines)
print(result, file=file)
print("\n---\n", file=file)
def compare(*args, directory: Path, min_speed: Optional[str] = None):
compare_args = ['pyperf', 'compare_to', '--table', '--table-format=md', *args]
if min_speed:
compare_args.extend(['--min-speed', min_speed])
return subprocess.check_output(
compare_args,
cwd=directory,
text=True,
)
def run(
configs: List[Dict[str, Environment]],
file: IO[str],
debug: bool = False,
min_speed: Optional[str] = None,
) -> None:
result_directory = Path(tempfile.mkdtemp())
results = []
current = 1
total = sum(1 for config in configs for _ in config.items())
def iterate(env_name, status):
print(
f'Iteration: {env_name} ({current}/{total}) ({status})' + ' ' * 10,
end='\r',
flush=True,
)
for config in configs:
for env_name, env in config.items():
iterate(env_name, 'setting up')
with env.on_repo() as (python, env_vars):
iterate(env_name, 'running benchmarks')
args = [python, BENCHMARK_SCRIPT, '-o', env_name]
if debug:
args.append('--debug-single-value')
call(
args,
cwd=result_directory,
env=env_vars,
)
current += 1
results.append(compare(
*config.keys(),
directory=result_directory,
min_speed=min_speed
))
dump_results(results, file=file, min_speed=min_speed)
print('Results are available at:', result_directory)
def main() -> None:
parser = ArgumentParser()
parser.add_argument('--local-repo', default=CURRENT_REPO)
parser.add_argument('--local-branch', default=None)
parser.add_argument('--target-repo', default=CURRENT_REPO)
parser.add_argument('--target-branch', default=TARGET_BRANCH)
parser.add_argument(
'--fresh',
action='store_const',
const=GITHUB_URL,
dest='target_repo',
help='Clone the target repo from upstream GitHub URL',
)
parser.add_argument(
'--complex',
action='store_true',
help='Add a second run, with a complex python environment.',
)
parser.add_argument(
'--local-bin',
help='Run the suite with the given local binary in addition to'
' existing runners. (E.g --local-bin $(command -v xh))',
)
parser.add_argument(
'--file',
type=FileType('w'),
default=sys.stdout,
help='File to print the actual results',
)
parser.add_argument(
'--min-speed',
help='Minimum of speed in percent to consider that a '
'benchmark is significant'
)
parser.add_argument(
'--debug',
action='store_true',
)
options = parser.parse_args()
configs = []
base_config = {
options.target_branch: HTTPieEnvironment(options.target_repo, options.target_branch),
'this_branch': HTTPieEnvironment(options.local_repo, options.local_branch),
}
configs.append(base_config)
if options.complex:
complex_config = {
env_name
+ '-complex': dataclasses.replace(env, dependencies=ADDITIONAL_DEPS)
for env_name, env in base_config.items()
}
configs.append(complex_config)
if options.local_bin:
base_config['binary'] = LocalCommandEnvironment(options.local_bin)
run(configs, file=options.file, debug=options.debug, min_speed=options.min_speed)
if __name__ == '__main__':
main()

View File

@ -1,8 +1,8 @@
"""
HTTPie: command-line HTTP client for the API era.
HTTPie: modern, user-friendly command-line HTTP client for the API era.
"""
__version__ = '2.5.0'
__version__ = '3.0.0'
__author__ = 'Jakub Roztocil'
__licence__ = 'BSD'

13
httpie/adapters.py Normal file
View File

@ -0,0 +1,13 @@
from httpie.cli.dicts import HTTPHeadersDict
from requests.adapters import HTTPAdapter
class HTTPieHTTPAdapter(HTTPAdapter):
def build_response(self, req, resp):
"""Wrap the original headers with the `HTTPHeadersDict`
to preserve multiple headers that have the same name"""
response = super().build_response(req, resp)
response.headers = HTTPHeadersDict(getattr(resp, 'headers', {}))
return response

View File

@ -15,7 +15,7 @@ from .argtypes import (
parse_format_options,
)
from .constants import (
HTTP_GET, HTTP_POST, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
HTTP_GET, HTTP_POST, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT,
OUTPUT_OPTIONS_DEFAULT_OFFLINE, OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED,
OUT_RESP_BODY, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RequestType,
SEPARATOR_CREDENTIALS,
@ -50,7 +50,64 @@ class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
# TODO: refactor and design type-annotated data structures
# for raw args + parsed args and keep things immutable.
class HTTPieArgumentParser(argparse.ArgumentParser):
class BaseHTTPieArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
super().__init__(*args, formatter_class=formatter_class, **kwargs)
self.env = None
self.args = None
self.has_stdin_data = False
self.has_input_data = False
# noinspection PyMethodOverriding
def parse_args(
self,
env: Environment,
args=None,
namespace=None
) -> argparse.Namespace:
self.env = env
self.args, no_options = self.parse_known_args(args, namespace)
if self.args.debug:
self.args.traceback = True
self.has_stdin_data = (
self.env.stdin
and not getattr(self.args, 'ignore_stdin', False)
and not self.env.stdin_isatty
)
self.has_input_data = self.has_stdin_data or getattr(self.args, 'raw', None) is not None
return self.args
# noinspection PyShadowingBuiltins
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
if hasattr(self, 'root'):
env = self.root.env
else:
env = self.env
if env is not None:
file = {
sys.stdout: env.stdout,
sys.stderr: env.stderr,
None: env.stderr
}.get(file, file)
if not hasattr(file, 'buffer') and isinstance(message, str):
message = message.encode(env.stdout_encoding)
super()._print_message(message, file)
class HTTPieManagerArgumentParser(BaseHTTPieArgumentParser):
def parse_known_args(self, args=None, namespace=None):
try:
return super().parse_known_args(args, namespace)
except SystemExit as exc:
if not hasattr(self, 'root') and exc.code == 2: # Argument Parser Error
raise argparse.ArgumentError(None, None)
raise
class HTTPieArgumentParser(BaseHTTPieArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`.
Handles all input (CLI args, file args, stdin), applies defaults,
@ -58,13 +115,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
"""
def __init__(self, *args, formatter_class=HTTPieHelpFormatter, **kwargs):
kwargs['add_help'] = False
super().__init__(*args, formatter_class=formatter_class, **kwargs)
self.env = None
self.args = None
self.has_stdin_data = False
self.has_input_data = False
def __init__(self, *args, **kwargs):
kwargs.setdefault('add_help', False)
super().__init__(*args, **kwargs)
# noinspection PyMethodOverriding
def parse_args(
@ -120,6 +173,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
}
def _process_url(self):
if self.args.url.startswith('://'):
# Paste URL & add space shortcut: `http ://pie.dev` → `http://pie.dev`
self.args.url = self.args.url[3:]
if not URL_SCHEME_RE.match(self.args.url):
if os.path.basename(self.env.program_name) == 'https':
scheme = 'https://'
@ -138,18 +194,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
else:
self.args.url = scheme + self.args.url
# noinspection PyShadowingBuiltins
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
sys.stdout: self.env.stdout,
sys.stderr: self.env.stderr,
None: self.env.stderr
}.get(file, file)
if not hasattr(file, 'buffer') and isinstance(message, str):
message = message.encode(self.env.stdout_encoding)
super()._print_message(message, file)
def _setup_standard_streams(self):
"""
Modify `env.stdout` and `env.stdout_isatty` based on args, if needed.
@ -252,6 +296,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
' --ignore-stdin is set.'
)
credentials.prompt_password(url.netloc)
if (credentials.key and credentials.value):
plugin.raw_auth = credentials.key + ":" + credentials.value
self.args.auth = plugin.get_auth(
username=credentials.key,
password=credentials.value,
@ -311,7 +359,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.error('Request body (from stdin, --raw or a file) and request '
'data (key=value) cannot be mixed. Pass '
'--ignore-stdin to let key/value take priority. '
'See https://httpie.org/doc#scripting for details.')
'See https://httpie.io/docs#scripting for details.')
def _guess_method(self):
"""Set `args.method` if not specified to either POST or GET
@ -361,7 +409,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
try:
request_items = RequestItems.from_args(
request_item_args=self.args.request_items,
as_form=self.args.form,
request_type=self.args.request_type,
)
except ParseError as e:
if self.args.traceback:
@ -412,8 +460,10 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.args.all = True
if self.args.output_options is None:
if self.args.verbose:
if self.args.verbose >= 2:
self.args.output_options = ''.join(OUTPUT_OPTIONS)
elif self.args.verbose == 1:
self.args.output_options = ''.join(BASE_OUTPUT_OPTIONS)
elif self.args.offline:
self.args.output_options = OUTPUT_OPTIONS_DEFAULT_OFFLINE
elif not self.env.stdout_isatty:
@ -457,7 +507,8 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
self.error('--continue requires --output to be specified')
def _process_format_options(self):
format_options = self.args.format_options or []
parsed_options = PARSED_DEFAULT_FORMAT_OPTIONS
for options_group in self.args.format_options or []:
for options_group in format_options:
parsed_options = parse_format_options(options_group, defaults=parsed_options)
self.args.format_options = parsed_options

View File

@ -57,12 +57,12 @@ class KeyValueArgType:
def __init__(self, *separators: str):
self.separators = separators
self.special_characters = set('\\')
self.special_characters = set()
for separator in separators:
self.special_characters.update(separator)
def __call__(self, s: str) -> KeyValueArg:
"""Parse raw string arg and return `self.key_value_class` instance.
"""Parse raw string arg and return `self.key_value_class` instance.
The best of `self.separators` is determined (first found, longest).
Back slash escaped characters aren't considered as separators
@ -113,7 +113,7 @@ class KeyValueArgType:
There are only two token types - strings and escaped characters:
>>> KeyValueArgType('=').tokenize(r'foo\=bar\\baz')
['foo', Escaped('='), 'bar', Escaped('\\'), 'baz']
['foo', Escaped('='), 'bar\\\\baz']
"""
tokens = ['']
@ -242,3 +242,19 @@ PARSED_DEFAULT_FORMAT_OPTIONS = parse_format_options(
s=','.join(DEFAULT_FORMAT_OPTIONS),
defaults=None,
)
def response_charset_type(encoding: str) -> str:
try:
''.encode(encoding)
except LookupError:
raise argparse.ArgumentTypeError(
f'{encoding!r} is not a supported encoding')
return encoding
def response_mime_type(mime_type: str) -> str:
if mime_type.count('/') != 1:
raise argparse.ArgumentTypeError(
f'{mime_type!r} doesnt look like a mime type; use type/subtype')
return mime_type

View File

@ -15,6 +15,7 @@ SEPARATOR_HEADER = ':'
SEPARATOR_HEADER_EMPTY = ';'
SEPARATOR_CREDENTIALS = ':'
SEPARATOR_PROXY = ':'
SEPARATOR_HEADER_EMBED = ':@'
SEPARATOR_DATA_STRING = '='
SEPARATOR_DATA_RAW_JSON = ':='
SEPARATOR_FILE_UPLOAD = '@'
@ -22,6 +23,7 @@ SEPARATOR_FILE_UPLOAD_TYPE = ';type=' # in already parsed file upload path only
SEPARATOR_DATA_EMBED_FILE_CONTENTS = '=@'
SEPARATOR_DATA_EMBED_RAW_JSON_FILE = ':=@'
SEPARATOR_QUERY_PARAM = '=='
SEPARATOR_QUERY_EMBED_FILE = '==@'
# Separators that become request data
SEPARATOR_GROUP_DATA_ITEMS = frozenset({
@ -40,13 +42,17 @@ SEPARATORS_GROUP_MULTIPART = frozenset({
# Separators for items whose value is a filename to be embedded
SEPARATOR_GROUP_DATA_EMBED_ITEMS = frozenset({
SEPARATOR_HEADER_EMBED,
SEPARATOR_QUERY_EMBED_FILE,
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
})
# Separators for raw JSON items
SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
# Separators for nested JSON items
SEPARATOR_GROUP_NESTED_JSON_ITEMS = frozenset([
SEPARATOR_DATA_STRING,
SEPARATOR_DATA_RAW_JSON,
SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
])
@ -54,7 +60,9 @@ SEPARATOR_GROUP_RAW_JSON_ITEMS = frozenset([
SEPARATOR_GROUP_ALL_ITEMS = frozenset({
SEPARATOR_HEADER,
SEPARATOR_HEADER_EMPTY,
SEPARATOR_HEADER_EMBED,
SEPARATOR_QUERY_PARAM,
SEPARATOR_QUERY_EMBED_FILE,
SEPARATOR_DATA_STRING,
SEPARATOR_DATA_RAW_JSON,
SEPARATOR_FILE_UPLOAD,
@ -67,12 +75,18 @@ OUT_REQ_HEAD = 'H'
OUT_REQ_BODY = 'B'
OUT_RESP_HEAD = 'h'
OUT_RESP_BODY = 'b'
OUT_RESP_META = 'm'
OUTPUT_OPTIONS = frozenset({
BASE_OUTPUT_OPTIONS = frozenset({
OUT_REQ_HEAD,
OUT_REQ_BODY,
OUT_RESP_HEAD,
OUT_RESP_BODY
OUT_RESP_BODY,
})
OUTPUT_OPTIONS = frozenset({
*BASE_OUTPUT_OPTIONS,
OUT_RESP_META,
})
# Pretty
@ -111,3 +125,9 @@ class RequestType(enum.Enum):
FORM = enum.auto()
MULTIPART = enum.auto()
JSON = enum.auto()
OPEN_BRACKET = '['
CLOSE_BRACKET = ']'
BACKSLASH = '\\'
HIGHLIGHTER = '^'

View File

@ -9,28 +9,29 @@ from .. import __doc__, __version__
from .argparser import HTTPieArgumentParser
from .argtypes import (
KeyValueArgType, SessionNameValidator,
readable_file_arg,
readable_file_arg, response_charset_type, response_mime_type,
)
from .constants import (
DEFAULT_FORMAT_OPTIONS, OUTPUT_OPTIONS,
DEFAULT_FORMAT_OPTIONS, BASE_OUTPUT_OPTIONS, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
OUT_RESP_BODY, OUT_RESP_HEAD, OUT_RESP_META, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
RequestType, SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING,
)
from .utils import LazyChoices
from ..output.formatters.colors import (
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
AUTO_STYLE, DEFAULT_STYLE, get_available_styles
)
from ..plugins.builtin import BuiltinAuthPlugin
from ..plugins.registry import plugin_manager
from ..sessions import DEFAULT_SESSIONS_DIR
from ..ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
from ..ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
parser = HTTPieArgumentParser(
prog='http',
description=f'{__doc__.strip()} <https://httpie.org>',
description=f'{__doc__.strip()} <https://httpie.io>',
epilog=dedent('''
For every --OPTION there is also a --no-OPTION that reverts OPTION
to its default value.
@ -41,6 +42,7 @@ parser = HTTPieArgumentParser(
'''),
)
parser.register('action', 'lazy_choices', LazyChoices)
#######################################################################
# Positional arguments.
@ -96,7 +98,7 @@ positional.add_argument(
':' HTTP headers:
Referer:http://httpie.org Cookie:foo=bar User-Agent:bacon/1.0
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
'==' URL parameters to be appended to the request URI:
@ -118,7 +120,7 @@ positional.add_argument(
'=@' A data field like '=', but takes a file path and embeds its content:
essay=@Documents/essay.txt
essay=@Documents/essay.txt
':=@' A raw JSON field like ':=', but takes a file path and embeds its content:
@ -247,32 +249,38 @@ output_processing.add_argument(
'''
)
def format_style_help(available_styles):
return '''
Output coloring style (default is "{default}"). It can be one of:
{available_styles}
The "{auto_style}" style follows your terminal's ANSI color styles.
For non-{auto_style} styles to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
'''.format(
default=DEFAULT_STYLE,
available_styles='\n'.join(
f' {line.strip()}'
for line in wrap(', '.join(available_styles), 60)
).strip(),
auto_style=AUTO_STYLE,
)
output_processing.add_argument(
'--style', '-s',
dest='style',
metavar='STYLE',
default=DEFAULT_STYLE,
choices=AVAILABLE_STYLES,
help='''
Output coloring style (default is "{default}"). It can be One of:
{available_styles}
The "{auto_style}" style follows your terminal's ANSI color styles.
For non-{auto_style} styles to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
'''.format(
default=DEFAULT_STYLE,
available_styles='\n'.join(
f' {line.strip()}'
for line in wrap(', '.join(sorted(AVAILABLE_STYLES)), 60)
).strip(),
auto_style=AUTO_STYLE,
)
action='lazy_choices',
getter=get_available_styles,
help_formatter=format_style_help
)
_sorted_kwargs = {
'action': 'append_const',
'const': SORTED_FORMAT_OPTIONS_STRING,
@ -309,6 +317,31 @@ output_processing.add_argument(
'''
)
output_processing.add_argument(
'--response-charset',
metavar='ENCODING',
type=response_charset_type,
help='''
Override the response encoding for terminal display purposes, e.g.:
--response-charset=utf8
--response-charset=big5
'''
)
output_processing.add_argument(
'--response-mime',
metavar='MIME_TYPE',
type=response_mime_type,
help='''
Override the response mime type for coloring and formatting for the terminal, e.g.:
--response-mime=application/json
--response-mime=text/xml
'''
)
output_processing.add_argument(
'--format-options',
@ -350,6 +383,7 @@ output_options.add_argument(
'{OUT_REQ_BODY}' request body
'{OUT_RESP_HEAD}' response headers
'{OUT_RESP_BODY}' response body
'{OUT_RESP_META}' response metadata
The default behaviour is '{OUTPUT_OPTIONS_DEFAULT}' (i.e., the response
headers and body is printed), if standard output is not redirected.
@ -368,6 +402,16 @@ output_options.add_argument(
'''
)
output_options.add_argument(
'--meta', '-m',
dest='output_options',
action='store_const',
const=OUT_RESP_META,
help=f'''
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
'''
)
output_options.add_argument(
'--body', '-b',
dest='output_options',
@ -382,12 +426,16 @@ output_options.add_argument(
output_options.add_argument(
'--verbose', '-v',
dest='verbose',
action='store_true',
action='count',
default=0,
help=f'''
Verbose output. Print the whole request as well as the response. Also print
any intermediary requests/responses (such as redirects).
It's a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
Verbose output. For the level one (with single `-v`/`--verbose`), print
the whole request as well as the response. Also print any intermediary
requests/responses (such as redirects). For the second level and higher,
print these as well as the response metadata.
Level one is a shortcut for: --all --print={''.join(BASE_OUTPUT_OPTIONS)}
Level two is a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
'''
)
output_options.add_argument(
@ -472,12 +520,14 @@ output_options.add_argument(
output_options.add_argument(
'--quiet', '-q',
action='store_true',
default=False,
action='count',
default=0,
help='''
Do not print to stdout or stderr.
Do not print to stdout or stderr, except for errors and warnings when provided once.
Provide twice to suppress warnings as well.
stdout is still redirected if --output is specified.
Flag doesn't affect behaviour of download beyond not printing to terminal.
'''
)
@ -527,36 +577,24 @@ auth = parser.add_argument_group(title='Authentication')
auth.add_argument(
'--auth', '-a',
default=None,
metavar='USER[:PASS]',
metavar='USER[:PASS] | TOKEN',
help='''
If only the username is provided (-a username), HTTPie will prompt
for the password.
For username/password based authentication mechanisms (e.g
basic auth or digest auth) if only the username is provided
(-a username), HTTPie will prompt for the password.
''',
)
class _AuthTypeLazyChoices:
# Needed for plugin testing
def __contains__(self, item):
return item in plugin_manager.get_auth_plugin_mapping()
def __iter__(self):
return iter(sorted(plugin_manager.get_auth_plugin_mapping().keys()))
_auth_plugins = plugin_manager.get_auth_plugins()
auth.add_argument(
'--auth-type', '-A',
choices=_AuthTypeLazyChoices(),
default=None,
help='''
def format_auth_help(auth_plugins_mapping):
auth_plugins = list(auth_plugins_mapping.values())
return '''
The authentication mechanism to be used. Defaults to "{default}".
{types}
'''.format(default=_auth_plugins[0].auth_type, types='\n '.join(
'''.format(default=auth_plugins[0].auth_type, types='\n '.join(
'"{type}": {name}{package}{description}'.format(
type=plugin.auth_type,
name=plugin.name,
@ -569,8 +607,18 @@ auth.add_argument(
'\n ' + ('\n '.join(wrap(plugin.description)))
)
)
for plugin in _auth_plugins
)),
for plugin in auth_plugins
))
auth.add_argument(
'--auth-type', '-A',
action='lazy_choices',
default=None,
getter=plugin_manager.get_auth_plugin_mapping,
sort=True,
cache=False,
help_formatter=format_auth_help,
)
auth.add_argument(
'--ignore-netrc',
@ -686,9 +734,11 @@ network.add_argument(
'--chunked',
default=False,
action='store_true',
help="""
help='''
Enable streaming via chunked transfer encoding.
The Transfer-Encoding header is set to chunked.
"""
'''
)
#######################################################################

View File

@ -1,15 +1,41 @@
from collections import OrderedDict
from requests.structures import CaseInsensitiveDict
from multidict import MultiDict, CIMultiDict
class RequestHeadersDict(CaseInsensitiveDict):
class BaseMultiDict(MultiDict):
"""
Headers are case-insensitive and multiple values are currently not supported.
Base class for all MultiDicts.
"""
class HTTPHeadersDict(CIMultiDict, BaseMultiDict):
"""
Headers are case-insensitive and multiple values are supported
through the `add()` API.
"""
def add(self, key, value):
"""
Add or update a new header.
If the given `value` is `None`, then all the previous
values will be overwritten and the value will be set
to `None`.
"""
if value is None:
self[key] = value
return None
# If the previous value for the given header is `None`
# then discard it since we are explicitly giving a new
# value for it.
if key in self and self.getone(key) is None:
self.popone(key)
super().add(key, value)
class RequestJSONDataDict(OrderedDict):
pass

344
httpie/cli/nested_json.py Normal file
View File

@ -0,0 +1,344 @@
from enum import Enum, auto
from typing import (
Any,
Iterator,
NamedTuple,
Optional,
List,
NoReturn,
Type,
Union,
)
from httpie.cli.constants import OPEN_BRACKET, CLOSE_BRACKET, BACKSLASH, HIGHLIGHTER
class HTTPieSyntaxError(ValueError):
def __init__(
self,
source: str,
token: Optional['Token'],
message: str,
message_kind: str = 'Syntax',
) -> None:
self.source = source
self.token = token
self.message = message
self.message_kind = message_kind
def __str__(self):
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
if self.token is not None:
lines.append(self.source)
lines.append(
' ' * (self.token.start)
+ HIGHLIGHTER * (self.token.end - self.token.start)
)
return '\n'.join(lines)
class TokenKind(Enum):
TEXT = auto()
NUMBER = auto()
LEFT_BRACKET = auto()
RIGHT_BRACKET = auto()
def to_name(self) -> str:
for key, value in OPERATORS.items():
if value is self:
return repr(key)
else:
return 'a ' + self.name.lower()
OPERATORS = {OPEN_BRACKET: TokenKind.LEFT_BRACKET, CLOSE_BRACKET: TokenKind.RIGHT_BRACKET}
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
class Token(NamedTuple):
kind: TokenKind
value: Union[str, int]
start: int
end: int
def assert_cant_happen() -> NoReturn:
raise ValueError('Unexpected value')
def check_escaped_int(value: str) -> str:
if not value.startswith(BACKSLASH):
raise ValueError('Not an escaped int')
try:
int(value[1:])
except ValueError as exc:
raise ValueError('Not an escaped int') from exc
else:
return value[1:]
def tokenize(source: str) -> Iterator[Token]:
cursor = 0
backslashes = 0
buffer = []
def send_buffer() -> Iterator[Token]:
nonlocal backslashes
if not buffer:
return None
value = ''.join(buffer)
for variation, kind in [
(int, TokenKind.NUMBER),
(check_escaped_int, TokenKind.TEXT),
]:
try:
value = variation(value)
except ValueError:
continue
else:
break
else:
kind = TokenKind.TEXT
yield Token(
kind, value, start=cursor - (len(buffer) + backslashes), end=cursor
)
buffer.clear()
backslashes = 0
def can_advance() -> bool:
return cursor < len(source)
while can_advance():
index = source[cursor]
if index in OPERATORS:
yield from send_buffer()
yield Token(OPERATORS[index], index, cursor, cursor + 1)
elif index == BACKSLASH and can_advance():
if source[cursor + 1] in SPECIAL_CHARS:
backslashes += 1
else:
buffer.append(index)
buffer.append(source[cursor + 1])
cursor += 1
else:
buffer.append(index)
cursor += 1
yield from send_buffer()
class PathAction(Enum):
KEY = auto()
INDEX = auto()
APPEND = auto()
# Pseudo action, used by the interpreter
SET = auto()
def to_string(self) -> str:
return self.name.lower()
class Path:
def __init__(
self,
kind: PathAction,
accessor: Optional[Union[str, int]] = None,
tokens: Optional[List[Token]] = None,
is_root: bool = False,
):
self.kind = kind
self.accessor = accessor
self.tokens = tokens or []
self.is_root = is_root
def reconstruct(self) -> str:
if self.kind is PathAction.KEY:
if self.is_root:
return str(self.accessor)
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
elif self.kind is PathAction.INDEX:
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
elif self.kind is PathAction.APPEND:
return OPEN_BRACKET + CLOSE_BRACKET
else:
assert_cant_happen()
def parse(source: str) -> Iterator[Path]:
"""
start: literal? path*
literal: TEXT | NUMBER
path:
key_path
| index_path
| append_path
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
append_path: LEFT_BRACKET RIGHT_BRACKET
"""
tokens = list(tokenize(source))
cursor = 0
def can_advance():
return cursor < len(tokens)
def expect(*kinds):
nonlocal cursor
assert len(kinds) > 0
if can_advance():
token = tokens[cursor]
cursor += 1
if token.kind in kinds:
return token
elif tokens:
token = tokens[-1]._replace(
start=tokens[-1].end + 0, end=tokens[-1].end + 1
)
else:
token = None
if len(kinds) == 1:
suffix = kinds[0].to_name()
else:
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
suffix += ' or ' + kinds[-1].to_name()
message = f'Expecting {suffix}'
raise HTTPieSyntaxError(source, token, message)
root = Path(PathAction.KEY, '', is_root=True)
if can_advance():
token = tokens[cursor]
if token.kind in {TokenKind.TEXT, TokenKind.NUMBER}:
token = expect(TokenKind.TEXT, TokenKind.NUMBER)
root.accessor = str(token.value)
root.tokens.append(token)
yield root
while can_advance():
path_tokens = []
path_tokens.append(expect(TokenKind.LEFT_BRACKET))
token = expect(
TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET
)
path_tokens.append(token)
if token.kind is TokenKind.RIGHT_BRACKET:
path = Path(PathAction.APPEND, tokens=path_tokens)
elif token.kind is TokenKind.TEXT:
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.NUMBER:
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
else:
assert_cant_happen()
yield path
JSON_TYPE_MAPPING = {
dict: 'object',
list: 'array',
int: 'number',
float: 'number',
str: 'string',
}
def interpret(context: Any, key: str, value: Any) -> Any:
cursor = context
paths = list(parse(key))
paths.append(Path(PathAction.SET, value))
def type_check(index: int, path: Path, expected_type: Type[Any]) -> None:
if not isinstance(cursor, expected_type):
if path.tokens:
pseudo_token = Token(
None, None, path.tokens[0].start, path.tokens[-1].end
)
else:
pseudo_token = None
cursor_type = JSON_TYPE_MAPPING.get(
type(cursor), type(cursor).__name__
)
required_type = JSON_TYPE_MAPPING[expected_type]
message = f"Can't perform {path.kind.to_string()!r} based access on "
message += repr(
''.join(path.reconstruct() for path in paths[:index])
)
message += (
f' which has a type of {cursor_type!r} but this operation'
)
message += f' requires a type of {required_type!r}.'
raise HTTPieSyntaxError(
key, pseudo_token, message, message_kind='Type'
)
def object_for(kind: str) -> Any:
if kind is PathAction.KEY:
return {}
elif kind in {PathAction.INDEX, PathAction.APPEND}:
return []
else:
assert_cant_happen()
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
if path.kind is PathAction.KEY:
type_check(index, path, dict)
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
cursor = cursor.setdefault(
path.accessor, object_for(next_path.kind)
)
elif path.kind is PathAction.INDEX:
type_check(index, path, list)
if path.accessor < 0:
raise HTTPieSyntaxError(
key,
path.tokens[1],
'Negative indexes are not supported.',
message_kind='Value',
)
cursor.extend([None] * (path.accessor - len(cursor) + 1))
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
if cursor[path.accessor] is None:
cursor[path.accessor] = object_for(next_path.kind)
cursor = cursor[path.accessor]
elif path.kind is PathAction.APPEND:
type_check(index, path, list)
if next_path.kind is PathAction.SET:
cursor.append(next_path.accessor)
break
cursor.append(object_for(next_path.kind))
cursor = cursor[-1]
else:
assert_cant_happen()
return context
def interpret_nested_json(pairs):
context = {}
for key, value in pairs:
interpret(context, key, value)
return context

View File

@ -1,28 +1,33 @@
import os
import functools
from typing import Callable, Dict, IO, List, Optional, Tuple, Union
from .argtypes import KeyValueArg
from .constants import (
SEPARATORS_GROUP_MULTIPART, SEPARATOR_DATA_EMBED_FILE_CONTENTS,
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
SEPARATOR_DATA_EMBED_RAW_JSON_FILE, SEPARATOR_GROUP_NESTED_JSON_ITEMS,
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
SEPARATOR_QUERY_PARAM,
SEPARATOR_HEADER_EMBED, SEPARATOR_QUERY_PARAM,
SEPARATOR_QUERY_EMBED_FILE, RequestType
)
from .dicts import (
MultipartRequestDataDict, RequestDataDict, RequestFilesDict,
RequestHeadersDict, RequestJSONDataDict,
BaseMultiDict, MultipartRequestDataDict, RequestDataDict,
RequestFilesDict, HTTPHeadersDict, RequestJSONDataDict,
RequestQueryParamsDict,
)
from .exceptions import ParseError
from ..utils import get_content_type, load_json_preserve_order
from .nested_json import interpret_nested_json
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split
class RequestItems:
def __init__(self, as_form=False):
self.headers = RequestHeadersDict()
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
def __init__(self, request_type: Optional[RequestType] = None):
self.headers = HTTPHeadersDict()
self.request_type = request_type
self.is_json = request_type is None or request_type is RequestType.JSON
self.data = RequestJSONDataDict() if self.is_json else RequestDataDict()
self.files = RequestFilesDict()
self.params = RequestQueryParamsDict()
# To preserve the order of fields in file upload multipart requests.
@ -32,9 +37,9 @@ class RequestItems:
def from_args(
cls,
request_item_args: List[KeyValueArg],
as_form=False,
request_type: Optional[RequestType] = None,
) -> 'RequestItems':
instance = cls(as_form=as_form)
instance = cls(request_type=request_type)
rules: Dict[str, Tuple[Callable, dict]] = {
SEPARATOR_HEADER: (
process_header_arg,
@ -44,10 +49,18 @@ class RequestItems:
process_empty_header_arg,
instance.headers,
),
SEPARATOR_HEADER_EMBED: (
process_embed_header_arg,
instance.headers,
),
SEPARATOR_QUERY_PARAM: (
process_query_param_arg,
instance.params,
),
SEPARATOR_QUERY_EMBED_FILE: (
process_embed_query_param_arg,
instance.params,
),
SEPARATOR_FILE_UPLOAD: (
process_file_upload_arg,
instance.files,
@ -60,24 +73,47 @@ class RequestItems:
process_data_embed_file_contents_arg,
instance.data,
),
SEPARATOR_GROUP_NESTED_JSON_ITEMS: (
process_data_nested_json_embed_args,
instance.data,
),
SEPARATOR_DATA_RAW_JSON: (
process_data_raw_json_embed_arg,
json_only(instance, process_data_raw_json_embed_arg),
instance.data,
),
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
process_data_embed_raw_json_file_arg,
json_only(instance, process_data_embed_raw_json_file_arg),
instance.data,
),
}
if instance.is_json:
json_item_args, request_item_args = split(
request_item_args,
lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
)
if json_item_args:
pairs = [
(arg.key, rules[arg.sep][0](arg))
for arg in json_item_args
]
processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS]
value = processor_func(pairs)
target_dict.update(value)
# Then handle all other items.
for arg in request_item_args:
processor_func, target_dict = rules[arg.sep]
value = processor_func(arg)
target_dict[arg.key] = value
if arg.sep in SEPARATORS_GROUP_MULTIPART:
instance.multipart_data[arg.key] = value
if isinstance(target_dict, BaseMultiDict):
target_dict.add(arg.key, value)
else:
target_dict[arg.key] = value
return instance
@ -88,6 +124,10 @@ def process_header_arg(arg: KeyValueArg) -> Optional[str]:
return arg.value or None
def process_embed_header_arg(arg: KeyValueArg) -> str:
return load_text_file(arg).rstrip('\n')
def process_empty_header_arg(arg: KeyValueArg) -> str:
if not arg.value:
return arg.value
@ -100,6 +140,10 @@ def process_query_param_arg(arg: KeyValueArg) -> str:
return arg.value
def process_embed_query_param_arg(arg: KeyValueArg) -> str:
return load_text_file(arg).rstrip('\n')
def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
parts = arg.value.split(SEPARATOR_FILE_UPLOAD_TYPE)
filename = parts[0]
@ -123,6 +167,29 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
return load_text_file(arg)
def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
if items.is_json:
return func
@functools.wraps(func)
def wrapper(*args, **kwargs) -> str:
try:
ret = func(*args, **kwargs)
except ParseError:
ret = None
# If it is a basic type, then allow it
if isinstance(ret, (str, int, float)):
return str(ret)
else:
raise ParseError(
'Can\'t use complex JSON value types with '
'--form/--multipart.'
)
return wrapper
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
contents = load_text_file(arg)
value = load_json(arg, contents)
@ -134,6 +201,10 @@ def process_data_raw_json_embed_arg(arg: KeyValueArg) -> JSONType:
return value
def process_data_nested_json_embed_args(pairs) -> Dict[str, JSONType]:
return interpret_nested_json(pairs)
def load_text_file(item: KeyValueArg) -> str:
path = item.value
try:
@ -150,6 +221,6 @@ def load_text_file(item: KeyValueArg) -> str:
def load_json(arg: KeyValueArg, contents: str) -> JSONType:
try:
return load_json_preserve_order(contents)
return load_json_preserve_order_and_dupe_keys(contents)
except ValueError as e:
raise ParseError(f'{arg.orig!r}: {e}')

53
httpie/cli/utils.py Normal file
View File

@ -0,0 +1,53 @@
import argparse
from typing import Any, Callable, Generic, Iterator, Iterable, Optional, TypeVar
T = TypeVar('T')
class LazyChoices(argparse.Action, Generic[T]):
def __init__(
self,
*args,
getter: Callable[[], Iterable[T]],
help_formatter: Optional[Callable[[T], str]] = None,
sort: bool = False,
cache: bool = True,
**kwargs
) -> None:
self.getter = getter
self.help_formatter = help_formatter
self.sort = sort
self.cache = cache
self._help: Optional[str] = None
self._obj: Optional[Iterable[T]] = None
super().__init__(*args, **kwargs)
self.choices = self
def load(self) -> T:
if self._obj is None or not self.cache:
self._obj = self.getter()
assert self._obj is not None
return self._obj
@property
def help(self) -> str:
if self._help is None and self.help_formatter is not None:
self._help = self.help_formatter(self.load())
return self._help
@help.setter
def help(self, value: Any) -> None:
self._help = value
def __contains__(self, item: Any) -> bool:
return item in self.load()
def __iter__(self) -> Iterator[T]:
if self.sort:
return iter(sorted(self.load()))
else:
return iter(self.load())
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)

View File

@ -3,19 +3,21 @@ import http.client
import json
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Callable, Iterable, Union
from typing import Any, Dict, Callable, Iterable
from urllib.parse import urlparse, urlunparse
import requests
# noinspection PyPackageRequirements
import urllib3
from . import __version__
from .cli.dicts import RequestHeadersDict
from .constants import UTF8
from .adapters import HTTPieHTTPAdapter
from .context import Environment
from .cli.dicts import HTTPHeadersDict
from .encoding import UTF8
from .models import RequestsMessage
from .plugins.registry import plugin_manager
from .sessions import get_httpie_session
from .ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
from .ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, HTTPieHTTPSAdapter
from .uploads import (
compress_request, prepare_request_body,
get_multipart_data_and_content_type,
@ -32,15 +34,15 @@ DEFAULT_UA = f'HTTPie/{__version__}'
def collect_messages(
env: Environment,
args: argparse.Namespace,
config_dir: Path,
request_body_read_callback: Callable[[bytes], None] = None,
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
) -> Iterable[RequestsMessage]:
httpie_session = None
httpie_session_headers = None
if args.session or args.session_read_only:
httpie_session = get_httpie_session(
config_dir=config_dir,
config_dir=env.config.directory,
session_name=args.session or args.session_read_only,
host=args.headers.get('Host'),
url=args.url,
@ -48,6 +50,7 @@ def collect_messages(
httpie_session_headers = httpie_session.headers
request_kwargs = make_request_kwargs(
env,
args=args,
base_headers=httpie_session_headers,
request_body_read_callback=request_body_read_callback
@ -79,6 +82,7 @@ def collect_messages(
request = requests.Request(**request_kwargs)
prepared_request = requests_session.prepare_request(request)
apply_missing_repeated_headers(prepared_request, request.headers)
if args.path_as_is:
prepared_request.url = ensure_path_as_is(
orig_url=args.url,
@ -152,6 +156,7 @@ def build_requests_session(
requests_session = requests.Session()
# Install our adapter.
http_adapter = HTTPieHTTPAdapter()
https_adapter = HTTPieHTTPSAdapter(
ciphers=ciphers,
verify=verify,
@ -160,6 +165,7 @@ def build_requests_session(
if ssl_version else None
),
)
requests_session.mount('http://', http_adapter)
requests_session.mount('https://', https_adapter)
# Install adapters from plugins.
@ -178,8 +184,8 @@ def dump_request(kwargs: dict):
f'\n>>> requests.request(**{repr_dict(kwargs)})\n\n')
def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
final_headers = RequestHeadersDict()
def finalize_headers(headers: HTTPHeadersDict) -> HTTPHeadersDict:
final_headers = HTTPHeadersDict()
for name, value in headers.items():
if value is not None:
# “leading or trailing LWS MAY be removed without
@ -190,12 +196,42 @@ def finalize_headers(headers: RequestHeadersDict) -> RequestHeadersDict:
if isinstance(value, str):
# See <https://github.com/httpie/httpie/issues/212>
value = value.encode()
final_headers[name] = value
final_headers.add(name, value)
return final_headers
def make_default_headers(args: argparse.Namespace) -> RequestHeadersDict:
default_headers = RequestHeadersDict({
def apply_missing_repeated_headers(
prepared_request: requests.PreparedRequest,
original_headers: HTTPHeadersDict
) -> None:
"""Update the given `prepared_request`'s headers with the original
ones. This allows the requests to be prepared as usual, and then later
merged with headers that are specified multiple times."""
new_headers = HTTPHeadersDict(prepared_request.headers)
for prepared_name, prepared_value in prepared_request.headers.items():
if prepared_name not in original_headers:
continue
original_keys, original_values = zip(*filter(
lambda item: item[0].casefold() == prepared_name.casefold(),
original_headers.items()
))
if prepared_value not in original_values:
# If the current value is not among the initial values
# set for this field, then it means that this field got
# overridden on the way, and we should preserve it.
continue
new_headers.popone(prepared_name)
new_headers.update(zip(original_keys, original_values))
prepared_request.headers = new_headers
def make_default_headers(args: argparse.Namespace) -> HTTPHeadersDict:
default_headers = HTTPHeadersDict({
'User-Agent': DEFAULT_UA
})
@ -238,9 +274,28 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
}
def json_dict_to_request_body(data: Dict[str, Any]) -> str:
# Propagate the top-level list if there is only one
# item in the object, with an en empty key.
if len(data) == 1:
[(key, value)] = data.items()
if key == '' and isinstance(value, list):
data = value
if data:
data = json.dumps(data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
data = ''
return data
def make_request_kwargs(
env: Environment,
args: argparse.Namespace,
base_headers: RequestHeadersDict = None,
base_headers: HTTPHeadersDict = None,
request_body_read_callback=lambda chunk: chunk
) -> dict:
"""
@ -252,12 +307,7 @@ def make_request_kwargs(
data = args.data
auto_json = data and not args.form
if (args.json or auto_json) and isinstance(data, dict):
if data:
data = json.dumps(data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
data = ''
data = json_dict_to_request_body(data)
# Finalize headers.
headers = make_default_headers(args)
@ -282,7 +332,8 @@ def make_request_kwargs(
'url': args.url,
'headers': headers,
'data': prepare_request_body(
body=data,
env,
data,
body_read_callback=request_body_read_callback,
chunked=args.chunked,
offline=args.offline,

View File

@ -1,4 +1,90 @@
import sys
from typing import Any, Optional, Iterable
is_windows = 'win32' in str(sys.platform).lower()
try:
from functools import cached_property
except ImportError:
# Can be removed once we drop Python <3.8 support.
# Taken from `django.utils.functional.cached_property`.
class cached_property:
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
A cached property can be made out of an existing method:
(e.g. ``url = cached_property(get_absolute_url)``).
The optional ``name`` argument is obsolete as of Python 3.6 and will be
deprecated in Django 4.0 (#30127).
"""
name = None
@staticmethod
def func(instance):
raise TypeError(
'Cannot use cached_property instance without calling '
'__set_name__() on it.'
)
def __init__(self, func, name=None):
self.real_func = func
self.__doc__ = getattr(func, '__doc__')
def __set_name__(self, owner, name):
if self.name is None:
self.name = name
self.func = self.real_func
elif name != self.name:
raise TypeError(
"Cannot assign the same cached_property to two different names "
"(%r and %r)." % (self.name, name)
)
def __get__(self, instance, cls=None):
"""
Call the function and put the return value in instance.__dict__ so that
subsequent attribute access on the instance returns the cached value
instead of calling cached_property.__get__().
"""
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
# importlib_metadata was a provisional module, so the APIs changed quite a few times
# between 3.8-3.10. It was also not included in the standard library until 3.8, so
# we install the backport for <3.8.
if sys.version_info >= (3, 8):
import importlib.metadata as importlib_metadata
else:
import importlib_metadata
def find_entry_points(entry_points: Any, group: str) -> Iterable[importlib_metadata.EntryPoint]:
if hasattr(entry_points, "select"): # Python 3.10+ / importlib_metadata >= 3.9.0
return entry_points.select(group=group)
else:
return set(entry_points.get(group, ()))
def get_dist_name(entry_point: importlib_metadata.EntryPoint) -> Optional[str]:
dist = getattr(entry_point, "dist", None)
if dist is not None: # Python 3.10+
return dist.name
match = entry_point.pattern.match(entry_point.value)
if not (match and match.group('module')):
return None
package = match.group('module').split('.')[0]
try:
metadata = importlib_metadata.metadata(package)
except importlib_metadata.PackageNotFoundError:
return None
else:
return metadata.get('name')

View File

@ -5,7 +5,7 @@ from typing import Union
from . import __version__
from .compat import is_windows
from .constants import UTF8
from .encoding import UTF8
ENV_XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
@ -128,3 +128,7 @@ class Config(BaseConfigDict):
@property
def default_options(self) -> list:
return self['default_options']
@property
def plugins_dir(self) -> Path:
return Path(self.get('plugins_dir', self.directory / 'plugins')).resolve()

View File

@ -1,2 +0,0 @@
# UTF-8 encoding name
UTF8 = 'utf-8'

View File

@ -1,7 +1,8 @@
import sys
import os
from contextlib import contextmanager
from pathlib import Path
from typing import IO, Optional
from typing import Iterator, IO, Optional
try:
@ -11,7 +12,7 @@ except ImportError:
from .compat import is_windows
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
from .constants import UTF8
from .encoding import UTF8
from .utils import repr_dict
@ -120,6 +121,19 @@ class Environment:
self._devnull = open(os.devnull, 'w+')
return self._devnull
@contextmanager
def as_silent(self) -> Iterator[None]:
original_stdout = self.stdout
original_stderr = self.stderr
try:
self.stdout = self.devnull
self.stderr = self.devnull
yield
finally:
self.stdout = original_stdout
self.stderr = original_stderr
def log_error(self, msg, level='error'):
assert level in ['error', 'warning']
self._orig_stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')

View File

@ -2,39 +2,40 @@ import argparse
import os
import platform
import sys
from typing import List, Optional, Tuple, Union
import socket
from typing import List, Optional, Union, Callable
import requests
from pygments import __version__ as pygments_version
from requests import __version__ as requests_version
from . import __version__ as httpie_version
from .cli.constants import OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, OUT_RESP_HEAD
from .cli.constants import OUT_REQ_BODY
from .cli.nested_json import HTTPieSyntaxError
from .client import collect_messages
from .context import Environment
from .downloads import Downloader
from .models import (
RequestsMessageKind,
OutputOptions,
)
from .output.writer import write_message, write_stream, MESSAGE_SEPARATOR_BYTES
from .plugins.registry import plugin_manager
from .status import ExitStatus, http_status_to_exit_status
from .utils import unwrap_context
# noinspection PyDefaultArgument
def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitStatus:
"""
The main function.
Pre-process args, handle some special types of invocations,
and run the main program with error handling.
Return exit status code.
"""
def raw_main(
parser: argparse.ArgumentParser,
main_program: Callable[[argparse.Namespace, Environment], ExitStatus],
args: List[Union[str, bytes]] = sys.argv,
env: Environment = Environment()
) -> ExitStatus:
program_name, *args = args
env.program_name = os.path.basename(program_name)
args = decode_raw_args(args, env.stdin_encoding)
plugin_manager.load_installed_plugins()
from .cli.definition import parser
plugin_manager.load_installed_plugins(env.config.plugins_dir)
if env.config.default_options:
args = env.config.default_options + args
@ -42,6 +43,21 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
include_debug_info = '--debug' in args
include_traceback = include_debug_info or '--traceback' in args
def handle_generic_error(e, annotation=None):
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg = (
f'{msg} while doing a {request.method}'
f' request to URL: {request.url}'
)
if annotation:
msg += annotation
env.log_error(f'{type(e).__name__}: {msg}')
if include_traceback:
raise
if include_debug_info:
print_debug_info(env)
if args == ['--debug']:
@ -54,6 +70,11 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
args=args,
env=env,
)
except HTTPieSyntaxError as exc:
env.stderr.write(str(exc) + "\n")
if include_traceback:
raise
exit_status = ExitStatus.ERROR
except KeyboardInterrupt:
env.stderr.write('\n')
if include_traceback:
@ -67,7 +88,7 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
exit_status = ExitStatus.ERROR
else:
try:
exit_status = program(
exit_status = main_program(
args=parsed_args,
env=env,
)
@ -91,38 +112,50 @@ def main(args: List[Union[str, bytes]] = sys.argv, env=Environment()) -> ExitSta
f'Too many redirects'
f' (--max-redirects={parsed_args.max_redirects}).'
)
except requests.exceptions.ConnectionError as exc:
annotation = None
original_exc = unwrap_context(exc)
if isinstance(original_exc, socket.gaierror):
if original_exc.errno == socket.EAI_AGAIN:
annotation = '\nCouldnt connect to a DNS server. Please check your connection and try again.'
elif original_exc.errno == socket.EAI_NONAME:
annotation = '\nCouldnt resolve the given hostname. Please check the URL and try again.'
propagated_exc = original_exc
else:
propagated_exc = exc
handle_generic_error(propagated_exc, annotation=annotation)
exit_status = ExitStatus.ERROR
except Exception as e:
# TODO: Further distinction between expected and unexpected errors.
msg = str(e)
if hasattr(e, 'request'):
request = e.request
if hasattr(request, 'url'):
msg = (
f'{msg} while doing a {request.method}'
f' request to URL: {request.url}'
)
env.log_error(f'{type(e).__name__}: {msg}')
if include_traceback:
raise
handle_generic_error(e)
exit_status = ExitStatus.ERROR
return exit_status
def get_output_options(
args: argparse.Namespace,
message: Union[requests.PreparedRequest, requests.Response]
) -> Tuple[bool, bool]:
return {
requests.PreparedRequest: (
OUT_REQ_HEAD in args.output_options,
OUT_REQ_BODY in args.output_options,
),
requests.Response: (
OUT_RESP_HEAD in args.output_options,
OUT_RESP_BODY in args.output_options,
),
}[type(message)]
def main(
args: List[Union[str, bytes]] = sys.argv,
env: Environment = Environment()
) -> ExitStatus:
"""
The main function.
Pre-process args, handle some special types of invocations,
and run the main program with error handling.
Return exit status code.
"""
from .cli.definition import parser
return raw_main(
parser=parser,
main_program=program,
args=args,
env=env
)
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
@ -153,31 +186,32 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
msg.is_body_upload_chunk = True
msg.body = chunk
msg.headers = initial_request.headers
write_message(requests_message=msg, env=env, args=args, with_body=True, with_headers=False)
msg_output_options = OutputOptions.from_message(msg, body=True, headers=False)
write_message(requests_message=msg, env=env, args=args, output_options=msg_output_options)
try:
if args.download:
args.follow = True # --download implies --follow.
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
downloader.pre_request(args.headers)
messages = collect_messages(args=args, config_dir=env.config.directory,
messages = collect_messages(env, args=args,
request_body_read_callback=request_body_read_callback)
force_separator = False
prev_with_body = False
# Process messages as theyre generated
for message in messages:
is_request = isinstance(message, requests.PreparedRequest)
with_headers, with_body = get_output_options(args=args, message=message)
do_write_body = with_body
if prev_with_body and (with_headers or with_body) and (force_separator or not env.stdout_isatty):
output_options = OutputOptions.from_message(message, args.output_options)
do_write_body = output_options.body
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty):
# Separate after a previous message with body, if needed. See test_tokens.py.
separate()
force_separator = False
if is_request:
if output_options.kind is RequestsMessageKind.REQUEST:
if not initial_request:
initial_request = message
if with_body:
if output_options.body:
is_streamed_upload = not isinstance(message.body, (str, bytes))
do_write_body = not is_streamed_upload
force_separator = is_streamed_upload and env.stdout_isatty
@ -185,11 +219,12 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
final_response = message
if args.check_status or downloader:
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow)
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet):
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level='warning')
write_message(requests_message=message, env=env, args=args, with_headers=with_headers,
with_body=do_write_body)
prev_with_body = with_body
write_message(requests_message=message, env=env, args=args, output_options=output_options._replace(
body=do_write_body
))
prev_with_body = output_options.body
# Cleanup
if force_separator:
@ -227,6 +262,8 @@ def print_debug_info(env: Environment):
])
env.stderr.write('\n\n')
env.stderr.write(repr(env))
env.stderr.write('\n\n')
env.stderr.write(repr(plugin_manager))
env.stderr.write('\n')

View File

@ -14,7 +14,7 @@ from urllib.parse import urlsplit
import requests
from .models import HTTPResponse
from .models import HTTPResponse, OutputOptions
from .output.streams import RawStream
from .utils import humanize_bytes
@ -266,12 +266,11 @@ class Downloader:
total_size=total_size
)
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
stream = RawStream(
msg=HTTPResponse(final_response),
with_headers=False,
with_body=True,
output_options=output_options,
on_body_chunk_downloaded=self.chunk_downloaded,
chunk_size=1024 * 8
)
self._progress_reporter.output.write(
@ -324,7 +323,7 @@ class Downloader:
content_type=final_response.headers.get('Content-Type'),
)
unique_filename = get_unique_filename(filename)
return open(unique_filename, mode='a+b')
return open(unique_filename, buffering=0, mode='a+b')
class DownloadStatus:

50
httpie/encoding.py Normal file
View File

@ -0,0 +1,50 @@
from typing import Union, Tuple
from charset_normalizer import from_bytes
from charset_normalizer.constant import TOO_SMALL_SEQUENCE
UTF8 = 'utf-8'
ContentBytes = Union[bytearray, bytes]
def detect_encoding(content: ContentBytes) -> str:
"""
We default to UTF-8 if text too short, because the detection
can return a random encoding leading to confusing results
given the `charset_normalizer` version (< 2.0.5).
>>> too_short = ']"foo"'
>>> detected = from_bytes(too_short.encode()).best().encoding
>>> detected
'ascii'
>>> too_short.encode().decode(detected)
']"foo"'
"""
encoding = UTF8
if len(content) > TOO_SMALL_SEQUENCE:
match = from_bytes(bytes(content)).best()
if match:
encoding = match.encoding
return encoding
def smart_decode(content: ContentBytes, encoding: str) -> Tuple[str, str]:
"""Decode `content` using the given `encoding`.
If no `encoding` is provided, the best effort is to guess it from `content`.
Unicode errors are replaced.
"""
if not encoding:
encoding = detect_encoding(content)
return content.decode(encoding, 'replace'), encoding
def smart_encode(content: str, encoding: str) -> bytes:
"""Encode `content` using the given `encoding`.
Unicode errors are replaced.
"""
return content.encode(encoding, 'replace')

View File

View File

@ -0,0 +1,61 @@
import argparse
import sys
from typing import List, Union
from httpie.context import Environment
from httpie.status import ExitStatus
from httpie.manager.cli import parser
from httpie.manager.core import MSG_COMMAND_CONFUSION, program as main_program
def is_http_command(args: List[Union[str, bytes]], env: Environment) -> bool:
"""Check whether http/https parser can parse the arguments."""
from httpie.cli.definition import parser as http_parser
from httpie.manager.cli import COMMANDS
# If the user already selected a top-level sub-command, never
# show the http/https version. E.g httpie plugins pie.dev/post
if len(args) >= 1 and args[0] in COMMANDS:
return False
with env.as_silent():
try:
http_parser.parse_args(env=env, args=args)
except (Exception, SystemExit):
return False
else:
return True
def main(args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()) -> ExitStatus:
from httpie.core import raw_main
try:
return raw_main(
parser=parser,
main_program=main_program,
args=args,
env=env
)
except argparse.ArgumentError:
program_args = args[1:]
if is_http_command(program_args, env):
env.stderr.write(MSG_COMMAND_CONFUSION.format(args=' '.join(program_args)) + "\n")
return ExitStatus.ERROR
def program():
try:
exit_status = main()
except KeyboardInterrupt:
from httpie.status import ExitStatus
exit_status = ExitStatus.ERROR_CTRL_C
return exit_status
if __name__ == '__main__': # pragma: nocover
sys.exit(program())

112
httpie/manager/cli.py Normal file
View File

@ -0,0 +1,112 @@
from textwrap import dedent
from httpie.cli.argparser import HTTPieManagerArgumentParser
from httpie import __version__
COMMANDS = {
'plugins': {
'help': 'Manage HTTPie plugins.',
'install': [
'Install the given targets from PyPI '
'or from a local paths.',
{
'dest': 'targets',
'nargs': '+',
'help': 'targets to install'
}
],
'upgrade': [
'Upgrade the given plugins',
{
'dest': 'targets',
'nargs': '+',
'help': 'targets to upgrade'
}
],
'uninstall': [
'Uninstall the given HTTPie plugins.',
{
'dest': 'targets',
'nargs': '+',
'help': 'targets to install'
}
],
'list': [
'List all installed HTTPie plugins.'
],
},
}
def missing_subcommand(*args) -> str:
base = COMMANDS
for arg in args:
base = base[arg]
assert isinstance(base, dict)
subcommands = ', '.join(map(repr, base.keys()))
return f'Please specify one of these: {subcommands}'
def generate_subparsers(root, parent_parser, definitions):
action_dest = '_'.join(parent_parser.prog.split()[1:] + ['action'])
actions = parent_parser.add_subparsers(
dest=action_dest
)
for command, properties in definitions.items():
is_subparser = isinstance(properties, dict)
descr = properties.pop('help', None) if is_subparser else properties.pop(0)
command_parser = actions.add_parser(command, description=descr)
command_parser.root = root
if is_subparser:
generate_subparsers(root, command_parser, properties)
continue
for argument in properties:
command_parser.add_argument(**argument)
parser = HTTPieManagerArgumentParser(
prog='httpie',
description=dedent(
'''
Managing interface for the HTTPie itself. <https://httpie.io/docs#manager>
Be aware that you might be looking for http/https commands for sending
HTTP requests. This command is only available for managing the HTTTPie
plugins and the configuration around it.
'''
),
)
parser.add_argument(
'--debug',
action='store_true',
default=False,
help='''
Prints the exception traceback should one occur, as well as other
information useful for debugging HTTPie itself and for reporting bugs.
'''
)
parser.add_argument(
'--traceback',
action='store_true',
default=False,
help='''
Prints the exception traceback should one occur.
'''
)
parser.add_argument(
'--version',
action='version',
version=__version__,
help='''
Show version and exit.
'''
)
generate_subparsers(parser, parser, COMMANDS)

33
httpie/manager/core.py Normal file
View File

@ -0,0 +1,33 @@
import argparse
from httpie.context import Environment
from httpie.manager.plugins import PluginInstaller
from httpie.status import ExitStatus
from httpie.manager.cli import missing_subcommand, parser
MSG_COMMAND_CONFUSION = '''\
This command is only for managing HTTPie plugins.
To send a request, please use the http/https commands:
$ http {args}
$ https {args}
'''
# noinspection PyStringFormat
MSG_NAKED_INVOCATION = f'''\
{missing_subcommand()}
{MSG_COMMAND_CONFUSION}
'''.rstrip("\n").format(args='POST pie.dev/post hello=world')
def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
if args.action is None:
parser.error(MSG_NAKED_INVOCATION)
if args.action == 'plugins':
plugins = PluginInstaller(env, debug=args.debug)
return plugins.run(args.plugins_action, args)
return ExitStatus.SUCCESS

250
httpie/manager/plugins.py Normal file
View File

@ -0,0 +1,250 @@
import argparse
import os
import subprocess
import sys
import textwrap
import re
import shutil
from collections import defaultdict
from contextlib import suppress
from pathlib import Path
from typing import Tuple, Optional, List
from httpie.manager.cli import parser, missing_subcommand
from httpie.compat import importlib_metadata, get_dist_name
from httpie.context import Environment
from httpie.status import ExitStatus
from httpie.utils import as_site
PEP_503 = re.compile(r"[-_.]+")
class PluginInstaller:
def __init__(self, env: Environment, debug: bool = False) -> None:
self.env = env
self.dir = env.config.plugins_dir
self.debug = debug
self.setup_plugins_dir()
def setup_plugins_dir(self) -> None:
try:
self.dir.mkdir(
exist_ok=True,
parents=True
)
except OSError:
self.env.stderr.write(
f'Couldn\'t create "{self.dir!s}"'
' directory for plugin installation.'
' Please re-check the permissions for that directory,'
' and if needed, allow write-access.'
)
raise
def fail(
self,
command: str,
target: Optional[str] = None,
reason: Optional[str] = None
) -> ExitStatus:
message = f'Can\'t {command}'
if target:
message += f' {target!r}'
if reason:
message += f': {reason}'
self.env.stderr.write(message + '\n')
return ExitStatus.ERROR
def pip(self, *args, **kwargs) -> subprocess.CompletedProcess:
options = {
'check': True,
'shell': False,
'stdout': self.env.stdout,
'stderr': subprocess.PIPE,
}
options.update(kwargs)
cmd = [sys.executable, '-m', 'pip', *args]
return subprocess.run(
cmd,
**options
)
def _install(self, targets: List[str], mode='install', **process_options) -> Tuple[
Optional[bytes], ExitStatus
]:
pip_args = [
'install',
f'--prefix={self.dir}',
'--no-warn-script-location',
]
if mode == 'upgrade':
pip_args.append('--upgrade')
try:
process = self.pip(
*pip_args,
*targets,
**process_options,
)
except subprocess.CalledProcessError as error:
reason = None
if error.stderr:
stderr = error.stderr.decode()
if self.debug:
self.env.stderr.write('Command failed: ')
self.env.stderr.write(' '.join(error.cmd) + '\n')
self.env.stderr.write(textwrap.indent(' ', stderr))
last_line = stderr.strip().splitlines()[-1]
severity, _, message = last_line.partition(': ')
if severity == 'ERROR':
reason = message
stdout = error.stdout
exit_status = self.fail(mode, ', '.join(targets), reason)
else:
stdout = process.stdout
exit_status = ExitStatus.SUCCESS
return stdout, exit_status
def install(self, targets: List[str]) -> ExitStatus:
self.env.stdout.write(f"Installing {', '.join(targets)}...\n")
self.env.stdout.flush()
_, exit_status = self._install(targets)
return exit_status
def _clear_metadata(self, targets: List[str]) -> None:
# Due to an outstanding pip problem[0], we have to get rid of
# existing metadata for old versions manually.
# [0]: https://github.com/pypa/pip/issues/10727
result_deps = defaultdict(list)
for child in as_site(self.dir).iterdir():
if child.suffix in {'.dist-info', '.egg-info'}:
name, _, version = child.stem.rpartition('-')
result_deps[name].append((version, child))
for target in targets:
name, _, version = target.rpartition('-')
name = PEP_503.sub("-", name).lower().replace('-', '_')
if name not in result_deps:
continue
for result_version, meta_path in result_deps[name]:
if version != result_version:
shutil.rmtree(meta_path)
def upgrade(self, targets: List[str]) -> ExitStatus:
self.env.stdout.write(f"Upgrading {', '.join(targets)}...\n")
self.env.stdout.flush()
raw_stdout, exit_status = self._install(
targets,
mode='upgrade',
stdout=subprocess.PIPE
)
if not raw_stdout:
return exit_status
stdout = raw_stdout.decode()
self.env.stdout.write(stdout)
installation_line = stdout.splitlines()[-1]
if installation_line.startswith('Successfully installed'):
self._clear_metadata(installation_line.split()[2:])
def _uninstall(self, target: str) -> Optional[ExitStatus]:
try:
distribution = importlib_metadata.distribution(target)
except importlib_metadata.PackageNotFoundError:
return self.fail('uninstall', target, 'package is not installed')
base_dir = Path(distribution.locate_file('.')).resolve()
if self.dir not in base_dir.parents:
# If the package is installed somewhere else (e.g on the site packages
# of the real python interpreter), than that means this package is not
# installed through us.
return self.fail('uninstall', target,
'package is not installed through httpie plugins'
' interface')
files = distribution.files
if files is None:
return self.fail('uninstall', target, 'couldn\'t locate the package')
# TODO: Consider handling failures here (e.g if it fails,
# just rever the operation and leave the site-packages
# in a proper shape).
for file in files:
with suppress(FileNotFoundError):
os.unlink(distribution.locate_file(file))
metadata_path = getattr(distribution, '_path', None)
if (
metadata_path
and metadata_path.exists()
and not any(metadata_path.iterdir())
):
metadata_path.rmdir()
self.env.stdout.write(f'Successfully uninstalled {target}\n')
def uninstall(self, targets: List[str]) -> ExitStatus:
# Unfortunately uninstall doesn't work with custom pip schemes. See:
# - https://github.com/pypa/pip/issues/5595
# - https://github.com/pypa/pip/issues/4575
# so we have to implement our own uninstalling logic. Which works
# on top of the importlib_metadata.
exit_code = ExitStatus.SUCCESS
for target in targets:
exit_code |= self._uninstall(target) or ExitStatus.SUCCESS
return ExitStatus(exit_code)
def list(self) -> None:
from httpie.plugins.registry import plugin_manager
known_plugins = defaultdict(list)
for entry_point in plugin_manager.iter_entry_points(self.dir):
ep_info = (entry_point.group, entry_point.name)
ep_name = get_dist_name(entry_point) or entry_point.module
known_plugins[ep_name].append(ep_info)
for plugin, entry_points in known_plugins.items():
self.env.stdout.write(plugin)
version = importlib_metadata.version(plugin)
if version is not None:
self.env.stdout.write(f' ({version})')
self.env.stdout.write('\n')
for group, entry_point in sorted(entry_points):
self.env.stdout.write(f' {entry_point} ({group})\n')
def run(
self,
action: Optional[str],
args: argparse.Namespace,
) -> ExitStatus:
from httpie.plugins.manager import enable_plugins
if action is None:
parser.error(missing_subcommand('plugins'))
with enable_plugins(self.dir):
if action == 'install':
status = self.install(args.targets)
elif action == 'upgrade':
status = self.upgrade(args.targets)
elif action == 'uninstall':
status = self.uninstall(args.targets)
elif action == 'list':
status = self.list()
return status or ExitStatus.SUCCESS

View File

@ -1,39 +1,48 @@
from abc import ABCMeta, abstractmethod
from typing import Iterable, Optional
import requests
from enum import Enum, auto
from typing import Iterable, Union, NamedTuple
from urllib.parse import urlsplit
from .constants import UTF8
from .utils import split_cookies
from .cli.constants import (
OUT_REQ_BODY,
OUT_REQ_HEAD,
OUT_RESP_BODY,
OUT_RESP_HEAD,
OUT_RESP_META
)
from .compat import cached_property
from .utils import split_cookies, parse_content_type_header
class HTTPMessage(metaclass=ABCMeta):
class HTTPMessage:
"""Abstract class for HTTP messages."""
def __init__(self, orig):
self._orig = orig
@abstractmethod
def iter_body(self, chunk_size: int) -> Iterable[bytes]:
"""Return an iterator over the body."""
raise NotImplementedError
@abstractmethod
def iter_lines(self, chunk_size: int) -> Iterable[bytes]:
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
raise NotImplementedError
@property
@abstractmethod
def headers(self) -> str:
"""Return a `str` with the message's headers."""
raise NotImplementedError
@property
@abstractmethod
def encoding(self) -> Optional[str]:
"""Return a `str` with the message's encoding, if known."""
def metadata(self) -> str:
"""Return metadata about the current message."""
raise NotImplementedError
@property
def body(self) -> bytes:
"""Return a `bytes` with the message's body."""
raise NotImplementedError()
@cached_property
def encoding(self) -> str:
ct, params = parse_content_type_header(self.content_type)
return params.get('charset', '')
@property
def content_type(self) -> str:
@ -78,19 +87,20 @@ class HTTPResponse(HTTPMessage):
)
headers.extend(
f'Set-Cookie: {cookie}'
for cookie in split_cookies(original.headers.get('Set-Cookie'))
for header, value in original.headers.items()
for cookie in split_cookies(value)
if header == 'Set-Cookie'
)
return '\r\n'.join(headers)
@property
def encoding(self):
return self._orig.encoding or UTF8
@property
def body(self):
# Only now the response body is fetched.
# Shouldn't be touched unless the body is actually needed.
return self._orig.content
def metadata(self) -> str:
data = {}
data['Elapsed time'] = str(self._orig.elapsed.total_seconds()) + 's'
return '\n'.join(
f'{key}: {value}'
for key, value in data.items()
)
class HTTPRequest(HTTPMessage):
@ -112,7 +122,7 @@ class HTTPRequest(HTTPMessage):
query=f'?{url.query}' if url.query else ''
)
headers = dict(self._orig.headers)
headers = self._orig.headers.copy()
if 'Host' not in self._orig.headers:
headers['Host'] = url.netloc.split('@')[-1]
@ -125,10 +135,6 @@ class HTTPRequest(HTTPMessage):
headers = '\r\n'.join(headers).strip()
return headers
@property
def encoding(self):
return UTF8
@property
def body(self):
body = self._orig.body
@ -136,3 +142,67 @@ class HTTPRequest(HTTPMessage):
# Happens with JSON/form request data parsed from the command line.
body = body.encode()
return body or b''
RequestsMessage = Union[requests.PreparedRequest, requests.Response]
class RequestsMessageKind(Enum):
REQUEST = auto()
RESPONSE = auto()
def infer_requests_message_kind(message: RequestsMessage) -> RequestsMessageKind:
if isinstance(message, requests.PreparedRequest):
return RequestsMessageKind.REQUEST
elif isinstance(message, requests.Response):
return RequestsMessageKind.RESPONSE
else:
raise TypeError(f"Unexpected message type: {type(message).__name__}")
OPTION_TO_PARAM = {
RequestsMessageKind.REQUEST: {
'headers': OUT_REQ_HEAD,
'body': OUT_REQ_BODY,
},
RequestsMessageKind.RESPONSE: {
'headers': OUT_RESP_HEAD,
'body': OUT_RESP_BODY,
'meta': OUT_RESP_META
}
}
class OutputOptions(NamedTuple):
kind: RequestsMessageKind
headers: bool
body: bool
meta: bool = False
def any(self):
return (
self.headers
or self.body
or self.meta
)
@classmethod
def from_message(
cls,
message: RequestsMessage,
raw_args: str = '',
**kwargs
):
kind = infer_requests_message_kind(message)
options = {
option: param in raw_args
for option, param in OPTION_TO_PARAM[kind].items()
}
options.update(kwargs)
return cls(
kind=kind,
**options
)

View File

@ -1,6 +1,7 @@
import json
from typing import Optional, Type
from typing import Optional, Type, Tuple
import pygments.formatters
import pygments.lexer
import pygments.lexers
import pygments.style
@ -9,11 +10,14 @@ import pygments.token
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.lexer import Lexer
from pygments.lexers.data import JsonLexer
from pygments.lexers.special import TextLexer
from pygments.lexers.text import HttpLexer as PygmentsHttpLexer
from pygments.util import ClassNotFound
from ...compat import is_windows
from ..lexers.json import EnhancedJsonLexer
from ..lexers.metadata import MetadataLexer
from ..ui.palette import SHADE_NAMES, get_color
from ...context import Environment
from ...plugins import FormatterPlugin
@ -21,14 +25,15 @@ from ...plugins import FormatterPlugin
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
DEFAULT_STYLE = AUTO_STYLE
SOLARIZED_STYLE = 'solarized' # Bundled here
if is_windows:
# Colors on Windows via colorama don't look that
# great and fruity seems to give the best result there.
DEFAULT_STYLE = 'fruity'
AVAILABLE_STYLES = set(pygments.styles.get_all_styles())
AVAILABLE_STYLES.add(SOLARIZED_STYLE)
AVAILABLE_STYLES.add(AUTO_STYLE)
BUNDLED_STYLES = {
SOLARIZED_STYLE,
AUTO_STYLE
}
def get_available_styles():
return BUNDLED_STYLES | set(pygments.styles.get_all_styles())
class ColorFormatter(FormatterPlugin):
@ -40,6 +45,7 @@ class ColorFormatter(FormatterPlugin):
"""
group_name = 'colors'
metadata_lexer = MetadataLexer()
def __init__(
self,
@ -58,22 +64,24 @@ class ColorFormatter(FormatterPlugin):
has_256_colors = env.colors == 256
if use_auto_style or not has_256_colors:
http_lexer = PygmentsHttpLexer()
formatter = TerminalFormatter()
body_formatter = header_formatter = TerminalFormatter()
precise = False
else:
http_lexer = SimplifiedHTTPLexer()
formatter = Terminal256Formatter(
style=self.get_style_class(color_scheme)
)
from ..lexers.http import SimplifiedHTTPLexer
header_formatter, body_formatter, precise = self.get_formatters(color_scheme)
http_lexer = SimplifiedHTTPLexer(precise=precise)
self.explicit_json = explicit_json # --json
self.formatter = formatter
self.header_formatter = header_formatter
self.body_formatter = body_formatter
self.http_lexer = http_lexer
self.metadata_lexer = MetadataLexer(precise=precise)
def format_headers(self, headers: str) -> str:
return pygments.highlight(
code=headers,
lexer=self.http_lexer,
formatter=self.formatter,
formatter=self.header_formatter,
).strip()
def format_body(self, body: str, mime: str) -> str:
@ -82,10 +90,17 @@ class ColorFormatter(FormatterPlugin):
body = pygments.highlight(
code=body,
lexer=lexer,
formatter=self.formatter,
formatter=self.body_formatter,
)
return body
def format_metadata(self, metadata: str) -> str:
return pygments.highlight(
code=metadata,
lexer=self.metadata_lexer,
formatter=self.header_formatter,
).strip()
def get_lexer_for_body(
self, mime: str,
body: str
@ -96,6 +111,25 @@ class ColorFormatter(FormatterPlugin):
body=body,
)
def get_formatters(self, color_scheme: str) -> Tuple[
pygments.formatter.Formatter,
pygments.formatter.Formatter,
bool
]:
if color_scheme in PIE_STYLES:
header_style, body_style = PIE_STYLES[color_scheme]
precise = True
else:
header_style = self.get_style_class(color_scheme)
body_style = header_style
precise = False
return (
Terminal256Formatter(style=header_style),
Terminal256Formatter(style=body_style),
precise
)
@staticmethod
def get_style_class(color_scheme: str) -> Type[pygments.style.Style]:
try:
@ -151,57 +185,14 @@ def get_lexer(
else:
lexer = pygments.lexers.get_lexer_by_name('json')
# Use our own JSON lexer: it supports JSON bodies preceded by non-JSON data
# as well as legit JSON bodies.
if isinstance(lexer, JsonLexer):
lexer = EnhancedJsonLexer()
return lexer
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between
their names and values than the original one bundled with Pygments
(:class:`pygments.lexers.text import HttpLexer`), especially when
Solarized color scheme is used.
"""
name = 'HTTP'
aliases = ['http']
filenames = ['*.http']
tokens = {
'root': [
# Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
pygments.lexer.bygroups(
pygments.token.Name.Function,
pygments.token.Text,
pygments.token.Name.Namespace,
pygments.token.Text,
pygments.token.Keyword.Reserved,
pygments.token.Operator,
pygments.token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
pygments.lexer.bygroups(
pygments.token.Keyword.Reserved, # 'HTTP'
pygments.token.Operator, # '/'
pygments.token.Number, # Version
pygments.token.Text,
pygments.token.Number, # Status code
pygments.token.Text,
pygments.token.Name.Exception, # Reason
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
pygments.token.Name.Attribute, # Name
pygments.token.Text,
pygments.token.Operator, # Colon
pygments.token.Text,
pygments.token.String # Value
))
]
}
class Solarized256Style(pygments.style.Style):
"""
solarized256
@ -272,3 +263,124 @@ class Solarized256Style(pygments.style.Style):
pygments.token.Token: BASE1,
pygments.token.Token.Other: ORANGE,
}
PIE_HEADER_STYLE = {
# HTTP line / Headers / Etc.
pygments.token.Name.Namespace: 'bold primary',
pygments.token.Keyword.Reserved: 'bold grey',
pygments.token.Operator: 'bold grey',
pygments.token.Number: 'bold grey',
pygments.token.Name.Function.Magic: 'bold green',
pygments.token.Name.Exception: 'bold green',
pygments.token.Name.Attribute: 'blue',
pygments.token.String: 'primary',
# HTTP Methods
pygments.token.Name.Function: 'bold grey',
pygments.token.Name.Function.HTTP.GET: 'bold green',
pygments.token.Name.Function.HTTP.HEAD: 'bold green',
pygments.token.Name.Function.HTTP.POST: 'bold yellow',
pygments.token.Name.Function.HTTP.PUT: 'bold orange',
pygments.token.Name.Function.HTTP.PATCH: 'bold orange',
pygments.token.Name.Function.HTTP.DELETE: 'bold red',
# HTTP status codes
pygments.token.Number.HTTP.INFO: 'bold aqua',
pygments.token.Number.HTTP.OK: 'bold green',
pygments.token.Number.HTTP.REDIRECT: 'bold yellow',
pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange',
pygments.token.Number.HTTP.SERVER_ERR: 'bold red',
# Metadata
pygments.token.Name.Decorator: 'grey',
pygments.token.Number.SPEED.FAST: 'bold green',
pygments.token.Number.SPEED.AVG: 'bold yellow',
pygments.token.Number.SPEED.SLOW: 'bold orange',
pygments.token.Number.SPEED.VERY_SLOW: 'bold red',
}
PIE_BODY_STYLE = {
# {}[]:
pygments.token.Punctuation: 'grey',
# Keys
pygments.token.Name.Tag: 'pink',
# Values
pygments.token.Literal.String: 'green',
pygments.token.Literal.String.Double: 'green',
pygments.token.Literal.Number: 'aqua',
pygments.token.Keyword: 'orange',
# Other stuff
pygments.token.Text: 'primary',
pygments.token.Name.Attribute: 'primary',
pygments.token.Name.Builtin: 'blue',
pygments.token.Name.Builtin.Pseudo: 'blue',
pygments.token.Name.Class: 'blue',
pygments.token.Name.Constant: 'orange',
pygments.token.Name.Decorator: 'blue',
pygments.token.Name.Entity: 'orange',
pygments.token.Name.Exception: 'yellow',
pygments.token.Name.Function: 'blue',
pygments.token.Name.Variable: 'blue',
pygments.token.String: 'aqua',
pygments.token.String.Backtick: 'secondary',
pygments.token.String.Char: 'aqua',
pygments.token.String.Doc: 'aqua',
pygments.token.String.Escape: 'red',
pygments.token.String.Heredoc: 'aqua',
pygments.token.String.Regex: 'red',
pygments.token.Number: 'aqua',
pygments.token.Operator: 'primary',
pygments.token.Operator.Word: 'green',
pygments.token.Comment: 'secondary',
pygments.token.Comment.Preproc: 'green',
pygments.token.Comment.Special: 'green',
pygments.token.Generic.Deleted: 'aqua',
pygments.token.Generic.Emph: 'italic',
pygments.token.Generic.Error: 'red',
pygments.token.Generic.Heading: 'orange',
pygments.token.Generic.Inserted: 'green',
pygments.token.Generic.Strong: 'bold',
pygments.token.Generic.Subheading: 'orange',
pygments.token.Token: 'primary',
pygments.token.Token.Other: 'orange',
}
def make_style(name, raw_styles, shade):
def format_value(value):
return ' '.join(
get_color(part, shade) or part
for part in value.split()
)
bases = (pygments.style.Style,)
data = {
'styles': {
key: format_value(value)
for key, value in raw_styles.items()
}
}
return type(name, bases, data)
def make_styles():
styles = {}
for shade, name in SHADE_NAMES.items():
styles[name] = [
make_style(name, style_map, shade)
for style_name, style_map in [
(f'Pie{name}HeaderStyle', PIE_HEADER_STYLE),
(f'Pie{name}BodyStyle', PIE_BODY_STYLE),
]
]
return styles
PIE_STYLES = make_styles()
BUNDLED_STYLES |= PIE_STYLES.keys()

View File

@ -17,15 +17,16 @@ class JSONFormatter(FormatterPlugin):
]
if (self.kwargs['explicit_json']
or any(token in mime for token in maybe_json)):
from ..utils import load_prefixed_json
try:
obj = json.loads(body)
data_prefix, json_obj = load_prefixed_json(body)
except ValueError:
pass # Invalid JSON, ignore.
else:
# Indent, sort keys by name, and avoid
# unicode escapes to improve readability.
body = json.dumps(
obj=obj,
body = data_prefix + json.dumps(
obj=json_obj,
sort_keys=self.format_options['json']['sort_keys'],
ensure_ascii=False,
indent=self.format_options['json']['indent']

View File

@ -1,34 +1,53 @@
import sys
from typing import TYPE_CHECKING, Optional
from ...constants import UTF8
from ...encoding import UTF8
from ...plugins import FormatterPlugin
if TYPE_CHECKING:
from xml.dom.minidom import Document
XML_DECLARATION_OPEN = '<?xml'
XML_DECLARATION_CLOSE = '?>'
def parse_xml(data: str) -> 'Document':
"""Parse given XML `data` string into an appropriate :class:`~xml.dom.minidom.Document` object."""
from defusedxml.minidom import parseString
return parseString(data)
def parse_declaration(raw_body: str) -> Optional[str]:
body = raw_body.strip()
# XMLDecl ::= '<?xml' DECL_CONTENT '?>'
if body.startswith(XML_DECLARATION_OPEN):
end = body.find(XML_DECLARATION_CLOSE)
if end != -1:
return body[:end + len(XML_DECLARATION_CLOSE)]
def pretty_xml(document: 'Document',
declaration: Optional[str] = None,
encoding: Optional[str] = UTF8,
indent: int = 2,
standalone: Optional[bool] = None) -> str:
indent: int = 2) -> str:
"""Render the given :class:`~xml.dom.minidom.Document` `document` into a prettified string."""
kwargs = {
'encoding': encoding or UTF8,
'indent': ' ' * indent,
}
if standalone is not None and sys.version_info >= (3, 9):
kwargs['standalone'] = standalone
body = document.toprettyxml(**kwargs).decode()
body = document.toprettyxml(**kwargs).decode(kwargs['encoding'])
# Remove blank lines automatically added by `toprettyxml()`.
return '\n'.join(line for line in body.splitlines() if line.strip())
lines = [line for line in body.splitlines() if line.strip()]
# xml.dom automatically adds the declaration, even if
# it is not present in the actual body. Remove it.
if len(lines) >= 1 and parse_declaration(lines[0]):
lines.pop(0)
if declaration:
lines.insert(0, declaration)
return '\n'.join(lines)
class XMLFormatter(FormatterPlugin):
@ -44,6 +63,7 @@ class XMLFormatter(FormatterPlugin):
from xml.parsers.expat import ExpatError
from defusedxml.common import DefusedXmlException
declaration = parse_declaration(body)
try:
parsed_body = parse_xml(body)
except ExpatError:
@ -54,6 +74,6 @@ class XMLFormatter(FormatterPlugin):
body = pretty_xml(parsed_body,
encoding=parsed_body.encoding,
indent=self.format_options['xml']['indent'],
standalone=parsed_body.standalone)
declaration=declaration)
return body

View File

View File

@ -0,0 +1,12 @@
def precise(lexer, precise_token, parent_token):
# Due to a pygments bug*, custom tokens will look bad
# on outside styles. Until it is fixed on upstream, we'll
# convey whether the client is using pie style or not
# through precise option and return more precise tokens
# depending on it's value.
#
# [0]: https://github.com/pygments/pygments/issues/1986
if precise_token is None or not lexer.options.get("precise"):
return parent_token
else:
return precise_token

View File

@ -0,0 +1,97 @@
import re
import pygments
from httpie.output.lexers.common import precise
RE_STATUS_LINE = re.compile(r'(\d{3})( +)(.+)')
STATUS_TYPES = {
'1': pygments.token.Number.HTTP.INFO,
'2': pygments.token.Number.HTTP.OK,
'3': pygments.token.Number.HTTP.REDIRECT,
'4': pygments.token.Number.HTTP.CLIENT_ERR,
'5': pygments.token.Number.HTTP.SERVER_ERR,
}
RESPONSE_TYPES = {
'GET': pygments.token.Name.Function.HTTP.GET,
'HEAD': pygments.token.Name.Function.HTTP.HEAD,
'POST': pygments.token.Name.Function.HTTP.POST,
'PUT': pygments.token.Name.Function.HTTP.PUT,
'PATCH': pygments.token.Name.Function.HTTP.PATCH,
'DELETE': pygments.token.Name.Function.HTTP.DELETE,
}
def http_response_type(lexer, match, ctx):
status_match = RE_STATUS_LINE.match(match.group())
if status_match is None:
return None
status_code, text, reason = status_match.groups()
status_type = precise(
lexer,
STATUS_TYPES.get(status_code[0]),
pygments.token.Number
)
groups = pygments.lexer.bygroups(
status_type,
pygments.token.Text,
status_type
)
yield from groups(lexer, status_match, ctx)
def request_method(lexer, match, ctx):
response_type = precise(
lexer,
RESPONSE_TYPES.get(match.group()),
pygments.token.Name.Function
)
yield match.start(), response_type, match.group()
class SimplifiedHTTPLexer(pygments.lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between
their names and values than the original one bundled with Pygments
(:class:`pygments.lexers.text import HttpLexer`), especially when
Solarized color scheme is used.
"""
name = 'HTTP'
aliases = ['http']
filenames = ['*.http']
tokens = {
'root': [
# Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
pygments.lexer.bygroups(
request_method,
pygments.token.Text,
pygments.token.Name.Namespace,
pygments.token.Text,
pygments.token.Keyword.Reserved,
pygments.token.Operator,
pygments.token.Number
)),
# Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(.+)',
pygments.lexer.bygroups(
pygments.token.Keyword.Reserved, # 'HTTP'
pygments.token.Operator, # '/'
pygments.token.Number, # Version
pygments.token.Text,
http_response_type, # Status code and Reason
)),
# Header
(r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
pygments.token.Name.Attribute, # Name
pygments.token.Text,
pygments.token.Operator, # Colon
pygments.token.Text,
pygments.token.String # Value
))
]
}

View File

@ -0,0 +1,31 @@
import re
from pygments.lexer import bygroups, using, RegexLexer
from pygments.lexers.data import JsonLexer
from pygments.token import Token
PREFIX_TOKEN = Token.Error
PREFIX_REGEX = r'[^{\["]+'
class EnhancedJsonLexer(RegexLexer):
"""
Enhanced JSON lexer for Pygments.
It adds support for eventual data prefixing the actual JSON body.
"""
name = 'JSON'
flags = re.IGNORECASE | re.DOTALL
tokens = {
'root': [
# Eventual non-JSON data prefix followed by actual JSON body.
# FIX: data prefix + number (integer or float) is not correctly handled.
(
fr'({PREFIX_REGEX})' + r'((?:[{\["]|true|false|null).+)',
bygroups(PREFIX_TOKEN, using(JsonLexer))
),
# JSON body.
(r'.+', using(JsonLexer)),
],
}

View File

@ -0,0 +1,57 @@
import pygments
from httpie.output.lexers.common import precise
SPEED_TOKENS = {
0.45: pygments.token.Number.SPEED.FAST,
1.00: pygments.token.Number.SPEED.AVG,
2.50: pygments.token.Number.SPEED.SLOW,
}
def speed_based_token(lexer, match, ctx):
try:
value = float(match.group())
except ValueError:
return pygments.token.Number
for limit, token in SPEED_TOKENS.items():
if value <= limit:
break
else:
token = pygments.token.Number.SPEED.VERY_SLOW
response_type = precise(
lexer,
token,
pygments.token.Number
)
yield match.start(), response_type, match.group()
class MetadataLexer(pygments.lexer.RegexLexer):
"""Simple HTTPie metadata lexer."""
tokens = {
'root': [
(
r'(Elapsed time)( *)(:)( *)(\d+\.\d+)(s)', pygments.lexer.bygroups(
pygments.token.Name.Decorator, # Name
pygments.token.Text,
pygments.token.Operator, # Colon
pygments.token.Text,
speed_based_token,
pygments.token.Name.Builtin # Value
)
),
# Generic item
(
r'(.*?)( *)(:)( *)(.+)', pygments.lexer.bygroups(
pygments.token.Name.Decorator, # Name
pygments.token.Text,
pygments.token.Operator, # Colon
pygments.token.Text,
pygments.token.Text # Value
)
),
]
}

View File

@ -51,3 +51,8 @@ class Formatting:
for p in self.enabled_plugins:
content = p.format_body(content, mime)
return content
def format_metadata(self, metadata: str) -> str:
for p in self.enabled_plugins:
metadata = p.format_metadata(metadata)
return metadata

View File

@ -1,11 +1,12 @@
from abc import ABCMeta, abstractmethod
from itertools import chain
from typing import Callable, Iterable, Union
from typing import Callable, Iterable, Optional, Union
from ..context import Environment
from ..constants import UTF8
from ..models import HTTPMessage
from .processing import Conversion, Formatting
from ..context import Environment
from ..encoding import smart_decode, smart_encode, UTF8
from ..models import HTTPMessage, OutputOptions
from ..utils import parse_content_type_header
BINARY_SUPPRESSED_NOTICE = (
@ -32,47 +33,55 @@ class BaseStream(metaclass=ABCMeta):
def __init__(
self,
msg: HTTPMessage,
with_headers=True,
with_body=True,
output_options: OutputOptions,
on_body_chunk_downloaded: Callable[[bytes], None] = None
):
"""
:param msg: a :class:`models.HTTPMessage` subclass
:param with_headers: if `True`, headers will be included
:param with_body: if `True`, body will be included
:param output_options: a :class:`OutputOptions` instance to represent
which parts of the message is printed.
"""
assert with_headers or with_body
assert output_options.any()
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
self.output_options = output_options
self.on_body_chunk_downloaded = on_body_chunk_downloaded
def get_headers(self) -> bytes:
"""Return the headers' bytes."""
return self.msg.headers.encode()
def get_metadata(self) -> bytes:
"""Return the message metadata."""
return self.msg.metadata.encode()
@abstractmethod
def iter_body(self) -> Iterable[bytes]:
"""Return an iterator over the message body."""
def __iter__(self) -> Iterable[bytes]:
"""Return an iterator over `self.msg`."""
if self.with_headers:
if self.output_options.headers:
yield self.get_headers()
yield b'\r\n\r\n'
if self.with_body:
if self.output_options.body:
try:
for chunk in self.iter_body():
yield chunk
if self.on_body_chunk_downloaded:
self.on_body_chunk_downloaded(chunk)
except DataSuppressedError as e:
if self.with_headers:
if self.output_options.headers:
yield b'\n'
yield e.message
if self.output_options.meta:
if self.output_options.body:
yield b'\n\n'
yield self.get_metadata()
yield b'\n\n'
class RawStream(BaseStream):
"""The message is streamed in chunks with no processing."""
@ -88,6 +97,9 @@ class RawStream(BaseStream):
return self.msg.iter_body(self.chunk_size)
ENCODING_GUESS_THRESHOLD = 3
class EncodedStream(BaseStream):
"""Encoded HTTP message stream.
@ -98,8 +110,20 @@ class EncodedStream(BaseStream):
"""
CHUNK_SIZE = 1
def __init__(self, env=Environment(), **kwargs):
def __init__(
self,
env=Environment(),
mime_overwrite: str = None,
encoding_overwrite: str = None,
**kwargs
):
super().__init__(**kwargs)
if mime_overwrite:
self.mime = mime_overwrite
else:
self.mime, _ = parse_content_type_header(self.msg.content_type)
self._encoding = encoding_overwrite or self.msg.encoding
self._encoding_guesses = []
if env.stdout_isatty:
# Use the encoding supported by the terminal.
output_encoding = env.stdout_encoding
@ -113,8 +137,32 @@ class EncodedStream(BaseStream):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
yield line.decode(self.msg.encoding) \
.encode(self.output_encoding, 'replace') + lf
line = self.decode_chunk(line)
yield smart_encode(line, self.output_encoding) + lf
def decode_chunk(self, raw_chunk: str) -> str:
chunk, guessed_encoding = smart_decode(raw_chunk, self.encoding)
self._encoding_guesses.append(guessed_encoding)
return chunk
@property
def encoding(self) -> Optional[str]:
if self._encoding:
return self._encoding
# If we find a reliable (used consecutively) encoding, than
# use it for the next iterations.
if len(self._encoding_guesses) < ENCODING_GUESS_THRESHOLD:
return None
guess_1, guess_2 = self._encoding_guesses[-2:]
if guess_1 == guess_2:
self._encoding = guess_1
return guess_1
@encoding.setter
def encoding(self, value) -> None:
self._encoding = value
class PrettyStream(EncodedStream):
@ -136,12 +184,15 @@ class PrettyStream(EncodedStream):
super().__init__(**kwargs)
self.formatting = formatting
self.conversion = conversion
self.mime = self.msg.content_type.split(';')[0]
def get_headers(self) -> bytes:
return self.formatting.format_headers(
self.msg.headers).encode(self.output_encoding)
def get_metadata(self) -> bytes:
return self.formatting.format_metadata(
self.msg.metadata).encode(self.output_encoding)
def iter_body(self) -> Iterable[bytes]:
first_chunk = True
iter_lines = self.msg.iter_lines(self.CHUNK_SIZE)
@ -167,9 +218,9 @@ class PrettyStream(EncodedStream):
if not isinstance(chunk, str):
# Text when a converter has been used,
# otherwise it will always be bytes.
chunk = chunk.decode(self.msg.encoding, 'replace')
chunk = self.decode_chunk(chunk)
chunk = self.formatting.format_body(content=chunk, mime=self.mime)
return chunk.encode(self.output_encoding, 'replace')
return smart_encode(chunk, self.output_encoding)
class BufferedPrettyStream(PrettyStream):

View File

161
httpie/output/ui/palette.py Normal file
View File

@ -0,0 +1,161 @@
# Copy the brand palette
from typing import Optional
COLOR_PALETTE = {
'transparent': 'transparent',
'current': 'currentColor',
'white': '#F5F5F0',
'black': '#1C1818',
'grey': {
'50': '#F5F5F0',
'100': '#EDEDEB',
'200': '#D1D1CF',
'300': '#B5B5B2',
'400': '#999999',
'500': '#7D7D7D',
'600': '#666663',
'700': '#4F4D4D',
'800': '#363636',
'900': '#1C1818',
'DEFAULT': '#7D7D7D',
},
'aqua': {
'50': '#E8F0F5',
'100': '#D6E3ED',
'200': '#C4D9E5',
'300': '#B0CCDE',
'400': '#9EBFD6',
'500': '#8CB4CD',
'600': '#7A9EB5',
'700': '#698799',
'800': '#597082',
'900': '#455966',
'DEFAULT': '#8CB4CD',
},
'purple': {
'50': '#F0E0FC',
'100': '#E3C7FA',
'200': '#D9ADF7',
'300': '#CC96F5',
'400': '#BF7DF2',
'500': '#B464F0',
'600': '#9E54D6',
'700': '#8745BA',
'800': '#70389E',
'900': '#5C2982',
'DEFAULT': '#B464F0',
},
'orange': {
'50': '#FFEDDB',
'100': '#FFDEBF',
'200': '#FFCFA3',
'300': '#FFBF87',
'400': '#FFB06B',
'500': '#FFA24E',
'600': '#F2913D',
'700': '#E3822B',
'800': '#D6701C',
'900': '#C75E0A',
'DEFAULT': '#FFA24E',
},
'red': {
'50': '#FFE0DE',
'100': '#FFC7C4',
'200': '#FFB0AB',
'300': '#FF968F',
'400': '#FF8075',
'500': '#FF665B',
'600': '#E34F45',
'700': '#C7382E',
'800': '#AD2117',
'900': '#910A00',
'DEFAULT': '#FF665B',
},
'blue': {
'50': '#DBE3FA',
'100': '#BFCFF5',
'200': '#A1B8F2',
'300': '#85A3ED',
'400': '#698FEB',
'500': '#4B78E6',
'600': '#426BD1',
'700': '#3B5EBA',
'800': '#3354A6',
'900': '#2B478F',
'DEFAULT': '#4B78E6',
},
'pink': {
'50': '#FFEBFF',
'100': '#FCDBFC',
'200': '#FCCCFC',
'300': '#FCBAFC',
'400': '#FAABFA',
'500': '#FA9BFA',
'600': '#DE85DE',
'700': '#C26EC2',
'800': '#A854A6',
'900': '#8C3D8A',
'DEFAULT': '#FA9BFA',
},
'green': {
'50': '#E3F7E8',
'100': '#CCF2D6',
'200': '#B5EDC4',
'300': '#A1E8B0',
'400': '#8AE09E',
'500': '#73DC8C',
'600': '#63C27A',
'700': '#52AB66',
'800': '#429154',
'900': '#307842',
'DEFAULT': '#73DC8C',
},
'yellow': {
'50': '#F7F7DB',
'100': '#F2F2BF',
'200': '#EDEDA6',
'300': '#E5E88A',
'400': '#E0E36E',
'500': '#DBDE52',
'600': '#CCCC3D',
'700': '#BABA29',
'800': '#ABA614',
'900': '#999400',
'DEFAULT': '#DBDE52',
},
}
# Grey is the same no matter shade for the colors
COLOR_PALETTE['grey'] = {
shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys()
}
COLOR_PALETTE['primary'] = {
'700': COLOR_PALETTE['black'],
'600': 'ansibrightblack',
'500': COLOR_PALETTE['white'],
}
COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'}
SHADE_NAMES = {
'500': 'pie-dark',
'600': 'pie',
'700': 'pie-light'
}
SHADES = [
'50',
*map(str, range(100, 1000, 100))
]
def get_color(color: str, shade: str) -> Optional[str]:
if color not in COLOR_PALETTE:
return None
color_code = COLOR_PALETTE[color]
if isinstance(color_code, dict) and shade in color_code:
return color_code[shade]
else:
return color_code

37
httpie/output/utils.py Normal file
View 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

View File

@ -2,10 +2,16 @@ import argparse
import errno
from typing import IO, TextIO, Tuple, Type, Union
import requests
from ..cli.dicts import HTTPHeadersDict
from ..context import Environment
from ..models import HTTPRequest, HTTPResponse
from ..models import (
HTTPRequest,
HTTPResponse,
HTTPMessage,
RequestsMessage,
RequestsMessageKind,
OutputOptions
)
from .processing import Conversion, Formatting
from .streams import (
BaseStream, BufferedPrettyStream, EncodedStream, PrettyStream, RawStream,
@ -17,21 +23,19 @@ MESSAGE_SEPARATOR_BYTES = MESSAGE_SEPARATOR.encode()
def write_message(
requests_message: Union[requests.PreparedRequest, requests.Response],
requests_message: RequestsMessage,
env: Environment,
args: argparse.Namespace,
with_headers=False,
with_body=False,
output_options: OutputOptions,
):
if not (with_body or with_headers):
if not output_options.any():
return
write_stream_kwargs = {
'stream': build_output_stream_for_message(
args=args,
env=env,
requests_message=requests_message,
with_body=with_body,
with_headers=with_headers,
output_options=output_options,
),
# NOTE: `env.stdout` will in fact be `stderr` with `--download`
'outfile': env.stdout,
@ -93,25 +97,25 @@ def write_stream_with_colors_win(
def build_output_stream_for_message(
args: argparse.Namespace,
env: Environment,
requests_message: Union[requests.PreparedRequest, requests.Response],
with_headers: bool,
with_body: bool,
requests_message: RequestsMessage,
output_options: OutputOptions,
):
message_type = {
RequestsMessageKind.REQUEST: HTTPRequest,
RequestsMessageKind.RESPONSE: HTTPResponse,
}[output_options.kind]
stream_class, stream_kwargs = get_stream_type_and_kwargs(
env=env,
args=args,
message_type=message_type,
headers=requests_message.headers
)
message_class = {
requests.PreparedRequest: HTTPRequest,
requests.Response: HTTPResponse,
}[type(requests_message)]
yield from stream_class(
msg=message_class(requests_message),
with_headers=with_headers,
with_body=with_body,
msg=message_type(requests_message),
output_options=output_options,
**stream_kwargs,
)
if (env.stdout_isatty and with_body
if (env.stdout_isatty and output_options.body and not output_options.meta
and not getattr(requests_message, 'is_body_upload_chunk', False)):
# Ensure a blank line after the response body.
# For terminal output only.
@ -120,37 +124,49 @@ def build_output_stream_for_message(
def get_stream_type_and_kwargs(
env: Environment,
args: argparse.Namespace
args: argparse.Namespace,
message_type: Type[HTTPMessage],
headers: HTTPHeadersDict,
) -> Tuple[Type['BaseStream'], dict]:
"""Pick the right stream type and kwargs for it based on `env` and `args`.
"""
is_stream = args.stream
if not is_stream and message_type is HTTPResponse:
# If this is a response, then check the headers for determining
# auto-streaming.
is_stream = headers.get('Content-Type') == 'text/event-stream'
if not env.stdout_isatty and not args.prettify:
stream_class = RawStream
stream_kwargs = {
'chunk_size': (
RawStream.CHUNK_SIZE_BY_LINE
if args.stream
if is_stream
else RawStream.CHUNK_SIZE
)
}
elif args.prettify:
stream_class = PrettyStream if args.stream else BufferedPrettyStream
stream_kwargs = {
'env': env,
'conversion': Conversion(),
'formatting': Formatting(
env=env,
groups=args.prettify,
color_scheme=args.style,
explicit_json=args.json,
format_options=args.format_options,
)
}
else:
stream_class = EncodedStream
stream_kwargs = {
'env': env
'env': env,
}
if message_type is HTTPResponse:
stream_kwargs.update({
'mime_overwrite': args.response_mime,
'encoding_overwrite': args.response_charset,
})
if args.prettify:
stream_class = PrettyStream if is_stream else BufferedPrettyStream
stream_kwargs.update({
'conversion': Conversion(),
'formatting': Formatting(
env=env,
groups=args.prettify,
color_scheme=args.style,
explicit_json=args.json,
format_options=args.format_options,
)
})
return stream_class, stream_kwargs

View File

@ -1,5 +1,7 @@
class BasePlugin:
from typing import Tuple
class BasePlugin:
# The name of the plugin, eg. "My auth".
name = None
@ -53,7 +55,7 @@ class AuthPlugin(BasePlugin):
# then this is `None`.
raw_auth = None
def get_auth(self, username=None, password=None):
def get_auth(self, username: str = None, password: str = None):
"""
If `auth_parse` is set to `True`, then `username`
and `password` contain the parsed credentials.
@ -93,7 +95,7 @@ class TransportPlugin(BasePlugin):
class ConverterPlugin(BasePlugin):
"""
Possibly converts response data for prettified terminal display.
Possibly converts binary response data for prettified terminal display.
See httpie-msgpack for an example converter plugin:
@ -101,14 +103,21 @@ class ConverterPlugin(BasePlugin):
"""
def __init__(self, mime):
def __init__(self, mime: str):
self.mime = mime
def convert(self, content_bytes):
def convert(self, body: bytes) -> Tuple[str, str]:
"""
Convert a binary body to a textual representation for the terminal
and return a tuple containing the new Content-Type and content, e.g.:
('application/json', '{}')
"""
raise NotImplementedError
@classmethod
def supports(cls, mime):
def supports(cls, mime: str) -> bool:
raise NotImplementedError
@ -146,3 +155,11 @@ class FormatterPlugin(BasePlugin):
"""
return content
def format_metadata(self, metadata: str) -> str:
"""Return processed `metadata`.
:param metadata: The metadata as text.
"""
return metadata

View File

@ -34,6 +34,16 @@ class HTTPBasicAuth(requests.auth.HTTPBasicAuth):
return f'Basic {token}'
class HTTPBearerAuth(requests.auth.AuthBase):
def __init__(self, token: str) -> None:
self.token = token
def __call__(self, request: requests.PreparedRequest) -> requests.PreparedRequest:
request.headers['Authorization'] = f'Bearer {self.token}'
return request
class BasicAuthPlugin(BuiltinAuthPlugin):
name = 'Basic HTTP auth'
auth_type = 'basic'
@ -56,3 +66,14 @@ class DigestAuthPlugin(BuiltinAuthPlugin):
password: str
) -> requests.auth.HTTPDigestAuth:
return requests.auth.HTTPDigestAuth(username, password)
class BearerAuthPlugin(BuiltinAuthPlugin):
name = 'Bearer HTTP Auth'
auth_type = 'bearer'
netrc_parse = False
auth_parse = False
# noinspection PyMethodOverriding
def get_auth(self, **kwargs) -> requests.auth.HTTPDigestAuth:
return HTTPBearerAuth(self.raw_auth)

View File

@ -1,23 +1,47 @@
import sys
import os
import warnings
from itertools import groupby
from operator import attrgetter
from typing import Dict, List, Type
from typing import Dict, List, Type, Iterator, Optional, ContextManager
from pathlib import Path
from contextlib import contextmanager, nullcontext
from pkg_resources import iter_entry_points
from ..compat import importlib_metadata, find_entry_points, get_dist_name
from . import AuthPlugin, ConverterPlugin, FormatterPlugin
from .base import BasePlugin, TransportPlugin
from ..utils import repr_dict, as_site
from . import AuthPlugin, ConverterPlugin, FormatterPlugin, TransportPlugin
from .base import BasePlugin
ENTRY_POINT_NAMES = [
'httpie.plugins.auth.v1',
'httpie.plugins.formatter.v1',
'httpie.plugins.converter.v1',
'httpie.plugins.transport.v1',
]
ENTRY_POINT_CLASSES = {
'httpie.plugins.auth.v1': AuthPlugin,
'httpie.plugins.converter.v1': ConverterPlugin,
'httpie.plugins.formatter.v1': FormatterPlugin,
'httpie.plugins.transport.v1': TransportPlugin
}
ENTRY_POINT_NAMES = list(ENTRY_POINT_CLASSES.keys())
@contextmanager
def _load_directory(plugins_dir: Path) -> Iterator[None]:
plugins_path = os.fspath(plugins_dir)
sys.path.insert(0, plugins_path)
try:
yield
finally:
sys.path.remove(plugins_path)
def enable_plugins(plugins_dir: Optional[Path]) -> ContextManager[None]:
if plugins_dir is None:
return nullcontext()
else:
return _load_directory(as_site(plugins_dir))
class PluginManager(list):
def register(self, *plugins: Type[BasePlugin]):
for plugin in plugins:
self.append(plugin)
@ -28,12 +52,28 @@ class PluginManager(list):
def filter(self, by_type=Type[BasePlugin]):
return [plugin for plugin in self if issubclass(plugin, by_type)]
def load_installed_plugins(self):
for entry_point_name in ENTRY_POINT_NAMES:
for entry_point in iter_entry_points(entry_point_name):
def iter_entry_points(self, directory: Optional[Path] = None):
with enable_plugins(directory):
eps = importlib_metadata.entry_points()
for entry_point_name in ENTRY_POINT_NAMES:
yield from find_entry_points(eps, group=entry_point_name)
def load_installed_plugins(self, directory: Optional[Path] = None):
for entry_point in self.iter_entry_points(directory):
plugin_name = get_dist_name(entry_point)
try:
plugin = entry_point.load()
plugin.package_name = entry_point.dist.key
self.register(entry_point.load())
except BaseException as exc:
warnings.warn(
f'While loading "{plugin_name}", an error ocurred: {exc}\n'
f'For uninstallations, please use either "httpie plugins uninstall {plugin_name}" '
f'or "pip uninstall {plugin_name}" (depending on how you installed it in the first '
'place).'
)
continue
plugin.package_name = plugin_name
self.register(plugin)
# Auth
def get_auth_plugins(self) -> List[Type[AuthPlugin]]:
@ -65,5 +105,13 @@ class PluginManager(list):
def get_transport_plugins(self) -> List[Type[TransportPlugin]]:
return self.filter(TransportPlugin)
def __str__(self):
return repr_dict({
'adapters': self.get_transport_plugins(),
'auth': self.get_auth_plugins(),
'converters': self.get_converters(),
'formatters': self.get_formatters(),
})
def __repr__(self):
return f'<PluginManager: {list(self)}>'
return f'<{type(self).__name__} {self}>'

View File

@ -1,5 +1,5 @@
from .manager import PluginManager
from .builtin import BasicAuthPlugin, DigestAuthPlugin
from .builtin import BasicAuthPlugin, DigestAuthPlugin, BearerAuthPlugin
from ..output.formatters.headers import HeadersFormatter
from ..output.formatters.json import JSONFormatter
from ..output.formatters.xml import XMLFormatter
@ -13,6 +13,7 @@ plugin_manager = PluginManager()
plugin_manager.register(
BasicAuthPlugin,
DigestAuthPlugin,
BearerAuthPlugin,
HeadersFormatter,
JSONFormatter,
XMLFormatter,

View File

@ -13,7 +13,7 @@ from urllib.parse import urlsplit
from requests.auth import AuthBase
from requests.cookies import RequestsCookieJar, create_cookie
from .cli.dicts import RequestHeadersDict
from .cli.dicts import HTTPHeadersDict
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
from .plugins.registry import plugin_manager
@ -52,7 +52,7 @@ def get_httpie_session(
class Session(BaseConfigDict):
helpurl = 'https://httpie.org/doc#sessions'
helpurl = 'https://httpie.io/docs#sessions'
about = 'HTTPie session file'
def __init__(self, path: Union[str, Path]):
@ -65,7 +65,7 @@ class Session(BaseConfigDict):
'password': None
}
def update_headers(self, request_headers: RequestHeadersDict):
def update_headers(self, request_headers: HTTPHeadersDict):
"""
Update the session headers with the request ones while ignoring
certain name prefixes.
@ -98,8 +98,8 @@ class Session(BaseConfigDict):
self['headers'] = dict(headers)
@property
def headers(self) -> RequestHeadersDict:
return RequestHeadersDict(self['headers'])
def headers(self) -> HTTPHeadersDict:
return HTTPHeadersDict(self['headers'])
@property
def cookies(self) -> RequestsCookieJar:
@ -112,7 +112,7 @@ class Session(BaseConfigDict):
@cookies.setter
def cookies(self, jar: RequestsCookieJar):
# <https://docs.python.org/2/library/cookielib.html#cookie-objects>
# <https://docs.python.org/3/library/cookielib.html#cookie-objects>
stored_attrs = ['value', 'path', 'secure', 'expires']
self['cookies'] = {}
for cookie in jar:

View File

@ -1,6 +1,6 @@
import ssl
from requests.adapters import HTTPAdapter
from httpie.adapters import HTTPAdapter
# noinspection PyPackageRequirements
from urllib3.util.ssl_ import (
DEFAULT_CIPHERS, create_urllib3_context,

View File

@ -1,15 +1,27 @@
import sys
import os
import zlib
from typing import Callable, IO, Iterable, Tuple, Union
import functools
from typing import Any, Callable, IO, Iterable, Optional, Tuple, Union, TYPE_CHECKING
from urllib.parse import urlencode
import requests
from requests.utils import super_len
from requests_toolbelt import MultipartEncoder
if TYPE_CHECKING:
from requests_toolbelt import MultipartEncoder
from .context import Environment
from .cli.dicts import MultipartRequestDataDict, RequestDataDict
from .compat import is_windows
class ChunkedUploadStream:
class ChunkedStream:
def __iter__(self) -> Iterable[Union[str, bytes]]:
raise NotImplementedError
class ChunkedUploadStream(ChunkedStream):
def __init__(self, stream: Iterable, callback: Callable):
self.callback = callback
self.stream = stream
@ -20,10 +32,10 @@ class ChunkedUploadStream:
yield chunk
class ChunkedMultipartUploadStream:
class ChunkedMultipartUploadStream(ChunkedStream):
chunk_size = 100 * 1024
def __init__(self, encoder: MultipartEncoder):
def __init__(self, encoder: 'MultipartEncoder'):
self.encoder = encoder
def __iter__(self) -> Iterable[Union[str, bytes]]:
@ -34,75 +46,157 @@ class ChunkedMultipartUploadStream:
yield chunk
def as_bytes(data: Union[str, bytes]) -> bytes:
if isinstance(data, str):
return data.encode()
else:
return data
CallbackT = Callable[[bytes], bytes]
def _wrap_function_with_callback(
func: Callable[..., Any],
callback: CallbackT
) -> Callable[..., Any]:
@functools.wraps(func)
def wrapped(*args, **kwargs):
chunk = func(*args, **kwargs)
callback(chunk)
return chunk
return wrapped
def is_stdin(file: IO) -> bool:
try:
file_no = file.fileno()
except Exception:
return False
else:
return file_no == sys.stdin.fileno()
READ_THRESHOLD = float(os.getenv("HTTPIE_STDIN_READ_WARN_THRESHOLD", 10.0))
def observe_stdin_for_data_thread(env: Environment, file: IO) -> None:
# Windows unfortunately does not support select() operation
# on regular files, like stdin in our use case.
# https://docs.python.org/3/library/select.html#select.select
if is_windows:
return None
# If the user configures READ_THRESHOLD to be 0, then
# disable this warning.
if READ_THRESHOLD == 0:
return None
import select
import threading
def worker():
can_read, _, _ = select.select([file], [], [], READ_THRESHOLD)
if not can_read:
env.stderr.write(
f'> warning: no stdin data read in {READ_THRESHOLD}s '
f'(perhaps you want to --ignore-stdin)\n'
f'> See: https://httpie.io/docs/cli/best-practices\n'
)
thread = threading.Thread(
target=worker
)
thread.start()
def _prepare_file_for_upload(
env: Environment,
file: Union[IO, 'MultipartEncoder'],
callback: CallbackT,
chunked: bool = False,
content_length_header_value: Optional[int] = None,
) -> Union[bytes, IO, ChunkedStream]:
if not super_len(file):
if is_stdin(file):
observe_stdin_for_data_thread(env, file)
# Zero-length -> assume stdin.
if content_length_header_value is None and not chunked:
# Read the whole stdin to determine `Content-Length`.
#
# TODO: Instead of opt-in --chunked, consider making
# `Transfer-Encoding: chunked` for STDIN opt-out via
# something like --no-chunked.
# This would be backwards-incompatible so wait until v3.0.0.
#
file = as_bytes(file.read())
else:
file.read = _wrap_function_with_callback(
file.read,
callback
)
if chunked:
from requests_toolbelt import MultipartEncoder
if isinstance(file, MultipartEncoder):
return ChunkedMultipartUploadStream(
encoder=file,
)
else:
return ChunkedUploadStream(
stream=file,
callback=callback,
)
else:
return file
def prepare_request_body(
body: Union[str, bytes, IO, MultipartEncoder, RequestDataDict],
body_read_callback: Callable[[bytes], bytes],
content_length_header_value: int = None,
chunked=False,
offline=False,
) -> Union[str, bytes, IO, MultipartEncoder, ChunkedUploadStream]:
is_file_like = hasattr(body, 'read')
if isinstance(body, RequestDataDict):
body = urlencode(body, doseq=True)
env: Environment,
raw_body: Union[str, bytes, IO, 'MultipartEncoder', RequestDataDict],
body_read_callback: CallbackT,
offline: bool = False,
chunked: bool = False,
content_length_header_value: Optional[int] = None,
) -> Union[bytes, IO, 'MultipartEncoder', ChunkedStream]:
is_file_like = hasattr(raw_body, 'read')
if isinstance(raw_body, (bytes, str)):
body = as_bytes(raw_body)
elif isinstance(raw_body, RequestDataDict):
body = as_bytes(urlencode(raw_body, doseq=True))
else:
body = raw_body
if offline:
if is_file_like:
return body.read()
return body
if not is_file_like:
if chunked:
body = ChunkedUploadStream(
# Pass the entire body as one chunk.
stream=(chunk.encode() for chunk in [body]),
callback=body_read_callback,
)
else:
# File-like object.
if not super_len(body):
# Zero-length -> assume stdin.
if content_length_header_value is None and not chunked:
#
# Read the whole stdin to determine `Content-Length`.
#
# TODO: Instead of opt-in --chunked, consider making
# `Transfer-Encoding: chunked` for STDIN opt-out via
# something like --no-chunked.
# This would be backwards-incompatible so wait until v3.0.0.
#
body = body.read()
return as_bytes(raw_body.read())
else:
orig_read = body.read
return body
def new_read(*args):
chunk = orig_read(*args)
body_read_callback(chunk)
return chunk
body.read = new_read
if chunked:
if isinstance(body, MultipartEncoder):
body = ChunkedMultipartUploadStream(
encoder=body,
)
else:
body = ChunkedUploadStream(
stream=body,
callback=body_read_callback,
)
return body
if is_file_like:
return _prepare_file_for_upload(
env,
body,
chunked=chunked,
callback=body_read_callback,
content_length_header_value=content_length_header_value
)
elif chunked:
return ChunkedUploadStream(
stream=iter([body]),
callback=body_read_callback
)
else:
return body
def get_multipart_data_and_content_type(
data: MultipartRequestDataDict,
boundary: str = None,
content_type: str = None,
) -> Tuple[MultipartEncoder, str]:
) -> Tuple['MultipartEncoder', str]:
from requests_toolbelt import MultipartEncoder
encoder = MultipartEncoder(
fields=data.items(),
boundary=boundary,

Some files were not shown because too many files have changed in this diff Show More