mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 01:03:27 +01:00
[Major] UI Enhancements (#1321)
* Refactor tests to use a text-based standard output. (#1318) * Implement new style `--help` (#1316) * Implement man page generation (#1317) * Implement rich progress bars. (#1324) * Man page deployment & isolation. (#1325) * Remove all unsorted usages in the CLI docs * Implement isolated mode for man page generation * Add a CI job for autogenerated files * Distribute man pages through PyPI * Pin the date for man pages. (#1326) * Hide suppressed arguments from --help/man pages (#1329) * Change download spinner to line (#1328) * Regenerate autogenerated files when pushed against to master. (#1339) * Highlight options (#1340) * Additional man page enhancements (#1341) * Group options by the parent category & highlight -o/--o * Display (and underline) the METAVAR on man pages. * Make help message processing more robust (#1342) * Inherit `help` from `short_help` * Don't mirror short_help directly. * Fixup the serialization * Use `pager` and `man` on `--manual` when applicable (#1343) * Run `man $program` on --manual * Page the output of `--manual` for systems that lack man pages * Improvements over progress bars (separate bar, status line, etc.) (#1346) * Redesign the --help layout. * Make our usage of rich compatible with 9.10.0 * Add `HTTPIE_NO_MAN_PAGES` * Make tests also patch os.get_terminal_size * Generate CLI spec from HTTPie & Man Page Hook (#1354) * Generate CLI spec from HTTPie & add man page hook * Use the full command space for the option headers
This commit is contained in:
parent
86f4bf4d0a
commit
ff6f1887b0
28
.github/workflows/autogenerated-files.yml
vendored
Normal file
28
.github/workflows/autogenerated-files.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: Update Autogenerated Files
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
regen-autogenerated-files:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
|
- run: make regen-all
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
id: cpr
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
with:
|
||||||
|
commit-message: "[automated] Update auto-generated files"
|
||||||
|
title: "[automated] Update auto-generated files"
|
||||||
|
delete-branch: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
33
.github/workflows/docs-update-install.yml
vendored
33
.github/workflows/docs-update-install.yml
vendored
@ -1,33 +0,0 @@
|
|||||||
name: Update & Install Docs
|
|
||||||
|
|
||||||
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@v3
|
|
||||||
- uses: actions/setup-python@v3
|
|
||||||
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 }}
|
|
19
Makefile
19
Makefile
@ -147,11 +147,6 @@ doc-check:
|
|||||||
mdl --git-recurse --style docs/markdownlint.rb .
|
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
|
# Publishing to PyPi
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@ -211,3 +206,17 @@ brew-test:
|
|||||||
|
|
||||||
@echo $(H1)Auditing…$(H1END)
|
@echo $(H1)Auditing…$(H1END)
|
||||||
brew audit --strict httpie
|
brew audit --strict httpie
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Regeneration
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
regen-all: regen-man-pages regen-install-methods
|
||||||
|
|
||||||
|
regen-man-pages: install
|
||||||
|
@echo $(H1)Regenerate man pages$(H1END)
|
||||||
|
$(VENV_PYTHON) extras/scripts/generate_man_pages.py
|
||||||
|
|
||||||
|
regen-install-methods:
|
||||||
|
@echo $(H1)Updating installation instructions in the docs$(H1END)
|
||||||
|
$(VENV_PYTHON) docs/installation/generate.py
|
||||||
|
600
extras/man/http.1
Normal file
600
extras/man/http.1
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
.TH http 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||||
|
.SH NAME
|
||||||
|
http
|
||||||
|
.SH SYNOPSIS
|
||||||
|
http [METHOD] URL [REQUEST_ITEM ...]
|
||||||
|
|
||||||
|
.SH DESCRIPTION
|
||||||
|
HTTPie: modern, user-friendly command-line HTTP client for the API era. <https://httpie.io>
|
||||||
|
.SH Positional Arguments
|
||||||
|
|
||||||
|
These arguments come after any flags and in the order they are listed here.
|
||||||
|
Only URL is required.
|
||||||
|
|
||||||
|
.IP "\fB\,METHOD\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||||
|
|
||||||
|
This argument can be omitted in which case HTTPie will use POST if there
|
||||||
|
is some data to be sent, otherwise GET:
|
||||||
|
|
||||||
|
$ http example.org # => GET
|
||||||
|
$ http example.org hello=world # => POST
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,URL\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The request URL. Scheme defaults to \'http://\' if the URL
|
||||||
|
does not include one. (You can override this with:\fB\,--default-scheme\/\fR=http/https)
|
||||||
|
|
||||||
|
You can also use a shorthand for localhost
|
||||||
|
|
||||||
|
$ http :3000 # => http://localhost:3000
|
||||||
|
$ http :/foo # => http://localhost/foo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,REQUEST_ITEM\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Optional key-value pairs to be included in the request. The separator used
|
||||||
|
determines the type:
|
||||||
|
|
||||||
|
\':\' HTTP headers:
|
||||||
|
|
||||||
|
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||||
|
|
||||||
|
\'==\' URL parameters to be appended to the request URI:
|
||||||
|
|
||||||
|
search==httpie
|
||||||
|
|
||||||
|
\'=\' Data fields to be serialized into a JSON object (with\fB\,--json\/\fR,\fB\,-j\/\fR)
|
||||||
|
or form data (with\fB\,--form\/\fR,\fB\,-f\/\fR):
|
||||||
|
|
||||||
|
name=HTTPie language=Python description=\'CLI HTTP client\'
|
||||||
|
|
||||||
|
\':=\' Non-string JSON data fields (only with\fB\,--json\/\fR,\fB\,-j\/\fR):
|
||||||
|
|
||||||
|
awesome:=true amount:=42 colors:=\'["red", "green", "blue"]\'
|
||||||
|
|
||||||
|
\'@\' Form file fields (only with\fB\,--form\/\fR or\fB\,--multipart\/\fR):
|
||||||
|
|
||||||
|
cv@\~/Documents/CV.pdf
|
||||||
|
cv@\'\~/Documents/CV.pdf;type=application/pdf\'
|
||||||
|
|
||||||
|
\'=@\' A data field like \'=\', but takes a file path and embeds its content:
|
||||||
|
|
||||||
|
essay=@Documents/essay.txt
|
||||||
|
|
||||||
|
\':=@\' A raw JSON field like \':=\', but takes a file path and embeds its content:
|
||||||
|
|
||||||
|
package:=@./package.json
|
||||||
|
|
||||||
|
You can use a backslash to escape a colliding separator in the field name:
|
||||||
|
|
||||||
|
field-name-with\\:colon=value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Predefined Content Types
|
||||||
|
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
(default) Data items from the command line are serialized as a JSON object.
|
||||||
|
The Content-Type and Accept headers are set to application/json
|
||||||
|
(if not specified).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--form\/\fR, \fB\,-f\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Data items from the command line are serialized as form fields.
|
||||||
|
|
||||||
|
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||||
|
specified). The presence of any file fields results in a
|
||||||
|
multipart/form-data request.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--multipart\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Similar to\fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--boundary\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with\fB\,--form\/\fR.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--raw\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
This option allows you to pass raw request data without extra processing
|
||||||
|
(as opposed to the structured request items syntax):
|
||||||
|
|
||||||
|
$ http\fB\,--raw\/\fR=\'data\' pie.dev/post
|
||||||
|
|
||||||
|
You can achieve the same by piping the data via stdin:
|
||||||
|
|
||||||
|
$ echo data | http pie.dev/post
|
||||||
|
|
||||||
|
Or have HTTPie load the raw data from a file:
|
||||||
|
|
||||||
|
$ http pie.dev/post @data.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Content Processing Options
|
||||||
|
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Content compressed (encoded) with Deflate algorithm.
|
||||||
|
The Content-Encoding header is set to deflate.
|
||||||
|
|
||||||
|
Compression is skipped if it appears that compression ratio is
|
||||||
|
negative. Compression can be forced by repeating the argument.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Output Processing
|
||||||
|
.IP "\fB\,--pretty\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Controls output processing. The value can be "none" to not prettify
|
||||||
|
the output (default for redirected output), "all" to apply both colors
|
||||||
|
and formatting (default for terminal output), "colors", or "format".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--style\/\fR, \fB\,-s\/\fR \fI\,STYLE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Output coloring style (default is "auto"). It can be one of:
|
||||||
|
|
||||||
|
auto, pie, pie-dark, pie-light, solarized
|
||||||
|
|
||||||
|
|
||||||
|
For finding out all available styles in your system, try:
|
||||||
|
|
||||||
|
$ http\fB\,--style\/\fR
|
||||||
|
|
||||||
|
The "auto" style follows your terminal\'s ANSI color styles.
|
||||||
|
For non-auto 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).
|
||||||
|
|
||||||
|
.IP "\fB\,--unsorted\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Disables all sorting while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR=headers.sort:false,json.sort_keys:false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--sorted\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR=headers.sort:true,json.sort_keys:true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--response-charset\/\fR \fI\,ENCODING\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Override the response encoding for terminal display purposes, e.g.:
|
||||||
|
|
||||||
|
\fB\,--response-charset\/\fR=utf8
|
||||||
|
\fB\,--response-charset\/\fR=big5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--response-mime\/\fR \fI\,MIME_TYPE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||||
|
|
||||||
|
\fB\,--response-mime\/\fR=application/json
|
||||||
|
\fB\,--response-mime\/\fR=text/xml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--format-options\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Controls output formatting. Only relevant when formatting is enabled
|
||||||
|
through (explicit or implied)\fB\,--pretty\/\fR=all or\fB\,--pretty\/\fR=format.
|
||||||
|
The following are the default options:
|
||||||
|
|
||||||
|
headers.sort:true
|
||||||
|
json.format:true
|
||||||
|
json.indent:4
|
||||||
|
json.sort_keys:true
|
||||||
|
xml.format:true
|
||||||
|
xml.indent:2
|
||||||
|
|
||||||
|
You may use this option multiple times, as well as specify multiple
|
||||||
|
comma-separated options at the same time. For example, this modifies the
|
||||||
|
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR json.sort_keys:false,json.indent:2
|
||||||
|
|
||||||
|
This is something you will typically put into your config file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Output Options
|
||||||
|
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
String specifying what the output should contain:
|
||||||
|
|
||||||
|
\'H\' request headers
|
||||||
|
\'B\' request body
|
||||||
|
\'h\' response headers
|
||||||
|
\'b\' response body
|
||||||
|
\'m\' response metadata
|
||||||
|
|
||||||
|
The default behaviour is \'hb\' (i.e., the response
|
||||||
|
headers and body is printed), if standard output is not redirected.
|
||||||
|
If the output is piped to another program or to a file, then only the
|
||||||
|
response body is printed by default.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response headers. Shortcut for\fB\,--print\/\fR=h.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--meta\/\fR, \fB\,-m\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response metadata. Shortcut for\fB\,--print\/\fR=m.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response body. Shortcut for\fB\,--print\/\fR=b.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--verbose\/\fR, \fB\,-v\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Verbose output. For the level one (with single \fB\,-v\/\fR`/\fB\,--verbose\/\fR`), 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:\fB\,--all\/\fR\fB\,--print\/\fR=BHbh
|
||||||
|
Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--all\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, only the final request/response is shown. Use this flag to show
|
||||||
|
any intermediary requests/responses as well. Intermediary requests include
|
||||||
|
followed redirects (with\fB\,--follow\/\fR), the first unauthorized request when
|
||||||
|
Digest auth is used \fB\,--auth\/\fR=digest), etc.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--history-print\/\fR, \fB\,-P\/\fR \fI\,WHAT\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The same as\fB\,--print\/\fR,\fB\,-p\/\fR but applies only to intermediary requests/responses
|
||||||
|
(such as redirects) when their inclusion is enabled with\fB\,--all\/\fR. If this
|
||||||
|
options is not specified, then they are formatted the same way as the final
|
||||||
|
response.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--stream\/\fR, \fB\,-S\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Always stream the response body by line, i.e., behave like `tail\fB\,-f\/\fR\'.
|
||||||
|
|
||||||
|
Without\fB\,--stream\/\fR and with\fB\,--pretty\/\fR (either set or implied),
|
||||||
|
HTTPie fetches the whole response before it outputs the processed data.
|
||||||
|
|
||||||
|
Set this option when you want to continuously display a prettified
|
||||||
|
long-lived response, such as one from the Twitter streaming API.
|
||||||
|
|
||||||
|
It is useful also without\fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||||
|
more often and in smaller chunks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--output\/\fR, \fB\,-o\/\fR \fI\,FILE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Save output to FILE instead of stdout. If\fB\,--download\/\fR is also set, then only
|
||||||
|
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||||
|
printed to stderr.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--download\/\fR, \fB\,-d\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Do not print the response body to stdout. Rather, download it and store it
|
||||||
|
in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||||
|
[filename]. This action is similar to the default behaviour of wget.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Resume an interrupted download. Note that the\fB\,--output\/\fR option needs to be
|
||||||
|
specified as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--quiet\/\fR, \fB\,-q\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
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\fB\,--output\/\fR is specified.
|
||||||
|
Flag doesn\'t affect behaviour of download beyond not printing to terminal.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Sessions
|
||||||
|
.IP "\fB\,--session\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Create, or reuse and update a session. Within a session, custom headers,
|
||||||
|
auth credential, as well as any cookies sent by the server persist between
|
||||||
|
requests.
|
||||||
|
|
||||||
|
Session files are stored in:
|
||||||
|
|
||||||
|
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||||
|
|
||||||
|
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||||
|
|
||||||
|
https://httpie.io/docs/cli/config-file-directory
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--session-read-only\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Create or read a session without updating it form the request/response
|
||||||
|
exchange.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Authentication
|
||||||
|
.IP "\fB\,--auth\/\fR, \fB\,-a\/\fR \fI\,USER[:PASS] | TOKEN\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
For username/password based authentication mechanisms (e.g
|
||||||
|
basic auth or digest auth) if only the username is provided
|
||||||
|
\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The authentication mechanism to be used. Defaults to "basic".
|
||||||
|
|
||||||
|
"basic": Basic HTTP auth
|
||||||
|
|
||||||
|
"digest": Digest HTTP auth
|
||||||
|
|
||||||
|
"bearer": Bearer HTTP Auth
|
||||||
|
|
||||||
|
For finding out all available authentication types in your system, try:
|
||||||
|
|
||||||
|
$ http\fB\,--auth-type\/\fR
|
||||||
|
|
||||||
|
.IP "\fB\,--ignore-netrc\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Ignore credentials from .netrc.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Network
|
||||||
|
.IP "\fB\,--offline\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Build the request and print it but don\'t actually send it.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
String mapping protocol to the URL of the proxy
|
||||||
|
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||||
|
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||||
|
and $HTTPS_proxy are supported as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--follow\/\fR, \fB\,-F\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Follow 30x Location redirects.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--max-redirects\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, requests have a limit of 30 redirects (works with\fB\,--follow\/\fR).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--max-headers\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The maximum number of response headers to be read before giving up (default 0, i.e., no limit).
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--timeout\/\fR \fI\,SECONDS\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The connection timeout of the request in seconds.
|
||||||
|
The default value is 0, i.e., there is no timeout limit.
|
||||||
|
This is not a time limit on the entire response download;
|
||||||
|
rather, an error is reported if the server has not issued a response for
|
||||||
|
timeout seconds (more precisely, if no bytes have been received on
|
||||||
|
the underlying socket for timeout seconds).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--check-status\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||||
|
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||||
|
exit with an error if the status indicates one.
|
||||||
|
|
||||||
|
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
|
||||||
|
status code, HTTPie exits with 4 or 5 respectively. If the response is a
|
||||||
|
3xx (Redirect) and\fB\,--follow\/\fR hasn\'t been set, then the exit status is 3.
|
||||||
|
Also an error message is written to stderr if stdout is redirected.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--path-as-is\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Bypass dot segment (/../ or /./) URL squashing.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--chunked\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Enable streaming via chunked transfer encoding. The Transfer-Encoding header is set to chunked.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH SSL
|
||||||
|
.IP "\fB\,--verify\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Set to "no" (or "false") to skip checking the host\'s SSL certificate.
|
||||||
|
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||||
|
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||||
|
variable instead.)
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--ssl\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The desired protocol version to use. This will default to
|
||||||
|
SSL v2.3 which will negotiate the highest protocol that both
|
||||||
|
the server and your installation of OpenSSL support. Available protocols
|
||||||
|
may vary depending on OpenSSL installation (only the supported ones
|
||||||
|
are shown here).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--ciphers\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
A string in the OpenSSL cipher list format. By default, the following
|
||||||
|
is used:
|
||||||
|
|
||||||
|
ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
You can specify a local cert to use as client side SSL certificate.
|
||||||
|
This file may either contain both private key and certificate or you may
|
||||||
|
specify\fB\,--cert-key\/\fR separately.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert-key\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The private key to use with SSL. Only needed if\fB\,--cert\/\fR is given and the
|
||||||
|
certificate file does not contain the private key.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert-key-pass\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The passphrase to be used to with the given private key. Only needed if\fB\,--cert-key\/\fR
|
||||||
|
is given and the key file requires a passphrase.
|
||||||
|
If not provided, you\'ll be prompted interactively.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Troubleshooting
|
||||||
|
.IP "\fB\,--ignore-stdin\/\fR, \fB\,-I\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Do not attempt to read stdin
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--help\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show this help message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--manual\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show the full manual.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--version\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show version and exit.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--traceback\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Prints the exception traceback should one occur.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--default-scheme\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The default scheme to use if not specified in the URL.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--debug\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Prints the exception traceback should one occur, as well as other
|
||||||
|
information useful for debugging HTTPie itself and for reporting bugs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
91
extras/man/httpie.1
Normal file
91
extras/man/httpie.1
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
.TH httpie 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||||
|
.SH NAME
|
||||||
|
httpie
|
||||||
|
.SH SYNOPSIS
|
||||||
|
httpie HOSTNAME SESSION_NAME_OR_PATH TARGET TARGET TARGET TARGET TARGET TARGET
|
||||||
|
|
||||||
|
.SH DESCRIPTION
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.SH httpie cli export-args
|
||||||
|
Export available options for the CLI
|
||||||
|
.IP "\fB\,-f\/\fR, \fB\,--format\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli sessions upgrade
|
||||||
|
Upgrade the given HTTPie session with the latest layout. A list of changes between different session versions can be found in the official documentation.
|
||||||
|
.IP "\fB\,HOSTNAME\/\fR"
|
||||||
|
|
||||||
|
The host this session belongs.
|
||||||
|
|
||||||
|
.IP "\fB\,SESSION_NAME_OR_PATH\/\fR"
|
||||||
|
|
||||||
|
The name or the path for the session that will be upgraded.
|
||||||
|
|
||||||
|
.IP "\fB\,--bind-cookies\/\fR"
|
||||||
|
|
||||||
|
Bind domainless cookies to the host that session belongs.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli sessions upgrade-all
|
||||||
|
Upgrade all named sessions with the latest layout. A list of changes between different session versions can be found in the official documentation.
|
||||||
|
.IP "\fB\,--bind-cookies\/\fR"
|
||||||
|
|
||||||
|
Bind domainless cookies to the host that session belongs.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli plugins install
|
||||||
|
Install the given targets from PyPI or from a local paths.
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to install
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli plugins upgrade
|
||||||
|
Upgrade the given plugins
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to upgrade
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli plugins uninstall
|
||||||
|
Uninstall the given HTTPie plugins.
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to install
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie cli plugins list
|
||||||
|
List all installed HTTPie plugins.
|
||||||
|
.PP
|
||||||
|
.SH httpie plugins install
|
||||||
|
Install the given targets from PyPI or from a local paths.
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to install
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie plugins upgrade
|
||||||
|
Upgrade the given plugins
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to upgrade
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie plugins uninstall
|
||||||
|
Uninstall the given HTTPie plugins.
|
||||||
|
.IP "\fB\,TARGET\/\fR"
|
||||||
|
|
||||||
|
targets to install
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH httpie plugins list
|
||||||
|
List all installed HTTPie plugins.
|
||||||
|
.PP
|
600
extras/man/https.1
Normal file
600
extras/man/https.1
Normal file
@ -0,0 +1,600 @@
|
|||||||
|
.TH https 1 "2022-03-08" "HTTPie 3.1.1.dev0" "HTTPie Manual"
|
||||||
|
.SH NAME
|
||||||
|
https
|
||||||
|
.SH SYNOPSIS
|
||||||
|
https [METHOD] URL [REQUEST_ITEM ...]
|
||||||
|
|
||||||
|
.SH DESCRIPTION
|
||||||
|
HTTPie: modern, user-friendly command-line HTTP client for the API era. <https://httpie.io>
|
||||||
|
.SH Positional Arguments
|
||||||
|
|
||||||
|
These arguments come after any flags and in the order they are listed here.
|
||||||
|
Only URL is required.
|
||||||
|
|
||||||
|
.IP "\fB\,METHOD\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||||
|
|
||||||
|
This argument can be omitted in which case HTTPie will use POST if there
|
||||||
|
is some data to be sent, otherwise GET:
|
||||||
|
|
||||||
|
$ http example.org # => GET
|
||||||
|
$ http example.org hello=world # => POST
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,URL\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The request URL. Scheme defaults to \'http://\' if the URL
|
||||||
|
does not include one. (You can override this with:\fB\,--default-scheme\/\fR=http/https)
|
||||||
|
|
||||||
|
You can also use a shorthand for localhost
|
||||||
|
|
||||||
|
$ http :3000 # => http://localhost:3000
|
||||||
|
$ http :/foo # => http://localhost/foo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,REQUEST_ITEM\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Optional key-value pairs to be included in the request. The separator used
|
||||||
|
determines the type:
|
||||||
|
|
||||||
|
\':\' HTTP headers:
|
||||||
|
|
||||||
|
Referer:https://httpie.io Cookie:foo=bar User-Agent:bacon/1.0
|
||||||
|
|
||||||
|
\'==\' URL parameters to be appended to the request URI:
|
||||||
|
|
||||||
|
search==httpie
|
||||||
|
|
||||||
|
\'=\' Data fields to be serialized into a JSON object (with\fB\,--json\/\fR,\fB\,-j\/\fR)
|
||||||
|
or form data (with\fB\,--form\/\fR,\fB\,-f\/\fR):
|
||||||
|
|
||||||
|
name=HTTPie language=Python description=\'CLI HTTP client\'
|
||||||
|
|
||||||
|
\':=\' Non-string JSON data fields (only with\fB\,--json\/\fR,\fB\,-j\/\fR):
|
||||||
|
|
||||||
|
awesome:=true amount:=42 colors:=\'["red", "green", "blue"]\'
|
||||||
|
|
||||||
|
\'@\' Form file fields (only with\fB\,--form\/\fR or\fB\,--multipart\/\fR):
|
||||||
|
|
||||||
|
cv@\~/Documents/CV.pdf
|
||||||
|
cv@\'\~/Documents/CV.pdf;type=application/pdf\'
|
||||||
|
|
||||||
|
\'=@\' A data field like \'=\', but takes a file path and embeds its content:
|
||||||
|
|
||||||
|
essay=@Documents/essay.txt
|
||||||
|
|
||||||
|
\':=@\' A raw JSON field like \':=\', but takes a file path and embeds its content:
|
||||||
|
|
||||||
|
package:=@./package.json
|
||||||
|
|
||||||
|
You can use a backslash to escape a colliding separator in the field name:
|
||||||
|
|
||||||
|
field-name-with\\:colon=value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Predefined Content Types
|
||||||
|
.IP "\fB\,--json\/\fR, \fB\,-j\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
(default) Data items from the command line are serialized as a JSON object.
|
||||||
|
The Content-Type and Accept headers are set to application/json
|
||||||
|
(if not specified).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--form\/\fR, \fB\,-f\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Data items from the command line are serialized as form fields.
|
||||||
|
|
||||||
|
The Content-Type is set to application/x-www-form-urlencoded (if not
|
||||||
|
specified). The presence of any file fields results in a
|
||||||
|
multipart/form-data request.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--multipart\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Similar to\fB\,--form\/\fR, but always sends a multipart/form-data request (i.e., even without files).
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--boundary\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Specify a custom boundary string for multipart/form-data requests. Only has effect only together with\fB\,--form\/\fR.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--raw\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
This option allows you to pass raw request data without extra processing
|
||||||
|
(as opposed to the structured request items syntax):
|
||||||
|
|
||||||
|
$ http\fB\,--raw\/\fR=\'data\' pie.dev/post
|
||||||
|
|
||||||
|
You can achieve the same by piping the data via stdin:
|
||||||
|
|
||||||
|
$ echo data | http pie.dev/post
|
||||||
|
|
||||||
|
Or have HTTPie load the raw data from a file:
|
||||||
|
|
||||||
|
$ http pie.dev/post @data.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Content Processing Options
|
||||||
|
.IP "\fB\,--compress\/\fR, \fB\,-x\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Content compressed (encoded) with Deflate algorithm.
|
||||||
|
The Content-Encoding header is set to deflate.
|
||||||
|
|
||||||
|
Compression is skipped if it appears that compression ratio is
|
||||||
|
negative. Compression can be forced by repeating the argument.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Output Processing
|
||||||
|
.IP "\fB\,--pretty\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Controls output processing. The value can be "none" to not prettify
|
||||||
|
the output (default for redirected output), "all" to apply both colors
|
||||||
|
and formatting (default for terminal output), "colors", or "format".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--style\/\fR, \fB\,-s\/\fR \fI\,STYLE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Output coloring style (default is "auto"). It can be one of:
|
||||||
|
|
||||||
|
auto, pie, pie-dark, pie-light, solarized
|
||||||
|
|
||||||
|
|
||||||
|
For finding out all available styles in your system, try:
|
||||||
|
|
||||||
|
$ http\fB\,--style\/\fR
|
||||||
|
|
||||||
|
The "auto" style follows your terminal\'s ANSI color styles.
|
||||||
|
For non-auto 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).
|
||||||
|
|
||||||
|
.IP "\fB\,--unsorted\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Disables all sorting while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR=headers.sort:false,json.sort_keys:false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--sorted\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR=headers.sort:true,json.sort_keys:true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--response-charset\/\fR \fI\,ENCODING\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Override the response encoding for terminal display purposes, e.g.:
|
||||||
|
|
||||||
|
\fB\,--response-charset\/\fR=utf8
|
||||||
|
\fB\,--response-charset\/\fR=big5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--response-mime\/\fR \fI\,MIME_TYPE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||||
|
|
||||||
|
\fB\,--response-mime\/\fR=application/json
|
||||||
|
\fB\,--response-mime\/\fR=text/xml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--format-options\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Controls output formatting. Only relevant when formatting is enabled
|
||||||
|
through (explicit or implied)\fB\,--pretty\/\fR=all or\fB\,--pretty\/\fR=format.
|
||||||
|
The following are the default options:
|
||||||
|
|
||||||
|
headers.sort:true
|
||||||
|
json.format:true
|
||||||
|
json.indent:4
|
||||||
|
json.sort_keys:true
|
||||||
|
xml.format:true
|
||||||
|
xml.indent:2
|
||||||
|
|
||||||
|
You may use this option multiple times, as well as specify multiple
|
||||||
|
comma-separated options at the same time. For example, this modifies the
|
||||||
|
settings to disable the sorting of JSON keys, and sets the indent size to 2:
|
||||||
|
|
||||||
|
\fB\,--format-options\/\fR json.sort_keys:false,json.indent:2
|
||||||
|
|
||||||
|
This is something you will typically put into your config file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Output Options
|
||||||
|
.IP "\fB\,--print\/\fR, \fB\,-p\/\fR \fI\,WHAT\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
String specifying what the output should contain:
|
||||||
|
|
||||||
|
\'H\' request headers
|
||||||
|
\'B\' request body
|
||||||
|
\'h\' response headers
|
||||||
|
\'b\' response body
|
||||||
|
\'m\' response metadata
|
||||||
|
|
||||||
|
The default behaviour is \'hb\' (i.e., the response
|
||||||
|
headers and body is printed), if standard output is not redirected.
|
||||||
|
If the output is piped to another program or to a file, then only the
|
||||||
|
response body is printed by default.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--headers\/\fR, \fB\,-h\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response headers. Shortcut for\fB\,--print\/\fR=h.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--meta\/\fR, \fB\,-m\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response metadata. Shortcut for\fB\,--print\/\fR=m.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--body\/\fR, \fB\,-b\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Print only the response body. Shortcut for\fB\,--print\/\fR=b.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--verbose\/\fR, \fB\,-v\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Verbose output. For the level one (with single \fB\,-v\/\fR`/\fB\,--verbose\/\fR`), 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:\fB\,--all\/\fR\fB\,--print\/\fR=BHbh
|
||||||
|
Level two is a shortcut for:\fB\,--all\/\fR\fB\,--print\/\fR=BHbhm
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--all\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, only the final request/response is shown. Use this flag to show
|
||||||
|
any intermediary requests/responses as well. Intermediary requests include
|
||||||
|
followed redirects (with\fB\,--follow\/\fR), the first unauthorized request when
|
||||||
|
Digest auth is used \fB\,--auth\/\fR=digest), etc.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--history-print\/\fR, \fB\,-P\/\fR \fI\,WHAT\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The same as\fB\,--print\/\fR,\fB\,-p\/\fR but applies only to intermediary requests/responses
|
||||||
|
(such as redirects) when their inclusion is enabled with\fB\,--all\/\fR. If this
|
||||||
|
options is not specified, then they are formatted the same way as the final
|
||||||
|
response.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--stream\/\fR, \fB\,-S\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Always stream the response body by line, i.e., behave like `tail\fB\,-f\/\fR\'.
|
||||||
|
|
||||||
|
Without\fB\,--stream\/\fR and with\fB\,--pretty\/\fR (either set or implied),
|
||||||
|
HTTPie fetches the whole response before it outputs the processed data.
|
||||||
|
|
||||||
|
Set this option when you want to continuously display a prettified
|
||||||
|
long-lived response, such as one from the Twitter streaming API.
|
||||||
|
|
||||||
|
It is useful also without\fB\,--pretty\/\fR: It ensures that the output is flushed
|
||||||
|
more often and in smaller chunks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--output\/\fR, \fB\,-o\/\fR \fI\,FILE\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Save output to FILE instead of stdout. If\fB\,--download\/\fR is also set, then only
|
||||||
|
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||||
|
printed to stderr.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--download\/\fR, \fB\,-d\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Do not print the response body to stdout. Rather, download it and store it
|
||||||
|
in a file. The filename is guessed unless specified with\fB\,--output\/\fR
|
||||||
|
[filename]. This action is similar to the default behaviour of wget.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--continue\/\fR, \fB\,-c\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Resume an interrupted download. Note that the\fB\,--output\/\fR option needs to be
|
||||||
|
specified as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--quiet\/\fR, \fB\,-q\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
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\fB\,--output\/\fR is specified.
|
||||||
|
Flag doesn\'t affect behaviour of download beyond not printing to terminal.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Sessions
|
||||||
|
.IP "\fB\,--session\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Create, or reuse and update a session. Within a session, custom headers,
|
||||||
|
auth credential, as well as any cookies sent by the server persist between
|
||||||
|
requests.
|
||||||
|
|
||||||
|
Session files are stored in:
|
||||||
|
|
||||||
|
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||||
|
|
||||||
|
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||||
|
|
||||||
|
https://httpie.io/docs/cli/config-file-directory
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--session-read-only\/\fR \fI\,SESSION_NAME_OR_PATH\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Create or read a session without updating it form the request/response
|
||||||
|
exchange.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Authentication
|
||||||
|
.IP "\fB\,--auth\/\fR, \fB\,-a\/\fR \fI\,USER[:PASS] | TOKEN\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
For username/password based authentication mechanisms (e.g
|
||||||
|
basic auth or digest auth) if only the username is provided
|
||||||
|
\fB\,-a\/\fR username), HTTPie will prompt for the password.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--auth-type\/\fR, \fB\,-A\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The authentication mechanism to be used. Defaults to "basic".
|
||||||
|
|
||||||
|
"basic": Basic HTTP auth
|
||||||
|
|
||||||
|
"digest": Digest HTTP auth
|
||||||
|
|
||||||
|
"bearer": Bearer HTTP Auth
|
||||||
|
|
||||||
|
For finding out all available authentication types in your system, try:
|
||||||
|
|
||||||
|
$ http\fB\,--auth-type\/\fR
|
||||||
|
|
||||||
|
.IP "\fB\,--ignore-netrc\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Ignore credentials from .netrc.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Network
|
||||||
|
.IP "\fB\,--offline\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Build the request and print it but don\'t actually send it.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--proxy\/\fR \fI\,PROTOCOL:PROXY_URL\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
String mapping protocol to the URL of the proxy
|
||||||
|
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||||
|
different protocols. The environment variables $ALL_PROXY, $HTTP_PROXY,
|
||||||
|
and $HTTPS_proxy are supported as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--follow\/\fR, \fB\,-F\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Follow 30x Location redirects.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--max-redirects\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, requests have a limit of 30 redirects (works with\fB\,--follow\/\fR).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--max-headers\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The maximum number of response headers to be read before giving up (default 0, i.e., no limit).
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--timeout\/\fR \fI\,SECONDS\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The connection timeout of the request in seconds.
|
||||||
|
The default value is 0, i.e., there is no timeout limit.
|
||||||
|
This is not a time limit on the entire response download;
|
||||||
|
rather, an error is reported if the server has not issued a response for
|
||||||
|
timeout seconds (more precisely, if no bytes have been received on
|
||||||
|
the underlying socket for timeout seconds).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--check-status\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||||
|
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||||
|
exit with an error if the status indicates one.
|
||||||
|
|
||||||
|
When the server replies with a 4xx (Client Error) or 5xx (Server Error)
|
||||||
|
status code, HTTPie exits with 4 or 5 respectively. If the response is a
|
||||||
|
3xx (Redirect) and\fB\,--follow\/\fR hasn\'t been set, then the exit status is 3.
|
||||||
|
Also an error message is written to stderr if stdout is redirected.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--path-as-is\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Bypass dot segment (/../ or /./) URL squashing.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--chunked\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Enable streaming via chunked transfer encoding. The Transfer-Encoding header is set to chunked.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH SSL
|
||||||
|
.IP "\fB\,--verify\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Set to "no" (or "false") to skip checking the host\'s SSL certificate.
|
||||||
|
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||||
|
for private certs. (Or you can set the REQUESTS_CA_BUNDLE environment
|
||||||
|
variable instead.)
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--ssl\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The desired protocol version to use. This will default to
|
||||||
|
SSL v2.3 which will negotiate the highest protocol that both
|
||||||
|
the server and your installation of OpenSSL support. Available protocols
|
||||||
|
may vary depending on OpenSSL installation (only the supported ones
|
||||||
|
are shown here).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--ciphers\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
A string in the OpenSSL cipher list format. By default, the following
|
||||||
|
is used:
|
||||||
|
|
||||||
|
ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
You can specify a local cert to use as client side SSL certificate.
|
||||||
|
This file may either contain both private key and certificate or you may
|
||||||
|
specify\fB\,--cert-key\/\fR separately.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert-key\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The private key to use with SSL. Only needed if\fB\,--cert\/\fR is given and the
|
||||||
|
certificate file does not contain the private key.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--cert-key-pass\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The passphrase to be used to with the given private key. Only needed if\fB\,--cert-key\/\fR
|
||||||
|
is given and the key file requires a passphrase.
|
||||||
|
If not provided, you\'ll be prompted interactively.
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
.SH Troubleshooting
|
||||||
|
.IP "\fB\,--ignore-stdin\/\fR, \fB\,-I\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Do not attempt to read stdin
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--help\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show this help message and exit.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--manual\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show the full manual.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--version\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Show version and exit.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--traceback\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Prints the exception traceback should one occur.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--default-scheme\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
The default scheme to use if not specified in the URL.
|
||||||
|
|
||||||
|
|
||||||
|
.IP "\fB\,--debug\/\fR"
|
||||||
|
|
||||||
|
|
||||||
|
Prints the exception traceback should one occur, as well as other
|
||||||
|
information useful for debugging HTTPie itself and for reporting bugs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
156
extras/scripts/generate_man_pages.py
Normal file
156
extras/scripts/generate_man_pages.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import re
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Iterator, Iterable
|
||||||
|
|
||||||
|
import httpie
|
||||||
|
from httpie.cli.definition import options as core_options
|
||||||
|
from httpie.cli.options import ParserSpec
|
||||||
|
from httpie.manager.cli import options as manager_options
|
||||||
|
from httpie.output.ui.rich_help import OptionsHighlighter, to_usage
|
||||||
|
from httpie.output.ui.rich_utils import render_as_string
|
||||||
|
from httpie.utils import split
|
||||||
|
|
||||||
|
|
||||||
|
# Escape certain characters so they are rendered properly on
|
||||||
|
# all terminals.
|
||||||
|
ESCAPE_MAP = {
|
||||||
|
"'": "\\'",
|
||||||
|
'~': '\\~',
|
||||||
|
'’': "\\'",
|
||||||
|
'\\': '\\\\',
|
||||||
|
}
|
||||||
|
ESCAPE_MAP = {ord(key): value for key, value in ESCAPE_MAP.items()}
|
||||||
|
|
||||||
|
EXTRAS_DIR = Path(__file__).parent.parent
|
||||||
|
MAN_PAGE_PATH = EXTRAS_DIR / 'man'
|
||||||
|
|
||||||
|
OPTION_HIGHLIGHT_RE = re.compile(
|
||||||
|
OptionsHighlighter.highlights[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
class ManPageBuilder:
|
||||||
|
def __init__(self):
|
||||||
|
self.source = []
|
||||||
|
|
||||||
|
def title_line(
|
||||||
|
self,
|
||||||
|
full_name: str,
|
||||||
|
program_name: str,
|
||||||
|
program_version: str,
|
||||||
|
last_edit_date: str,
|
||||||
|
) -> None:
|
||||||
|
self.source.append(
|
||||||
|
f'.TH {program_name} 1 "{last_edit_date}" '
|
||||||
|
f'"{full_name} {program_version}" "{full_name} Manual"'
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_name(self, program_name: str) -> None:
|
||||||
|
with self.section('NAME'):
|
||||||
|
self.write(program_name)
|
||||||
|
|
||||||
|
def write(self, text: str, *, bold: bool = False) -> None:
|
||||||
|
if bold:
|
||||||
|
text = '.B ' + text
|
||||||
|
self.source.append(text)
|
||||||
|
|
||||||
|
def separate(self) -> None:
|
||||||
|
self.source.append('.PP')
|
||||||
|
|
||||||
|
def add_options(self, options: Iterable[str], *, metavar: Optional[str] = None) -> None:
|
||||||
|
text = ", ".join(map(self.boldify, options))
|
||||||
|
if metavar:
|
||||||
|
text += f' {self.underline(metavar)}'
|
||||||
|
self.write(f'.IP "{text}"')
|
||||||
|
|
||||||
|
def build(self) -> str:
|
||||||
|
return '\n'.join(self.source)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def section(self, section_name: str) -> Iterator[None]:
|
||||||
|
self.write(f'.SH {section_name}')
|
||||||
|
self.in_section = True
|
||||||
|
yield
|
||||||
|
self.in_section = False
|
||||||
|
|
||||||
|
def underline(self, text: str) -> str:
|
||||||
|
return r'\fI\,{}\/\fR'.format(text)
|
||||||
|
|
||||||
|
def boldify(self, text: str) -> str:
|
||||||
|
return r'\fB\,{}\/\fR'.format(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_and_dedent(text: str) -> str:
|
||||||
|
lines = []
|
||||||
|
for should_act, line in enumerate(text.splitlines()):
|
||||||
|
# Only dedent after the first line.
|
||||||
|
if should_act:
|
||||||
|
if line.startswith(' '):
|
||||||
|
line = line[4:]
|
||||||
|
|
||||||
|
lines.append(line)
|
||||||
|
return '\n'.join(lines).translate(ESCAPE_MAP)
|
||||||
|
|
||||||
|
|
||||||
|
def to_man_page(program_name: str, spec: ParserSpec) -> str:
|
||||||
|
builder = ManPageBuilder()
|
||||||
|
|
||||||
|
builder.title_line(
|
||||||
|
full_name='HTTPie',
|
||||||
|
program_name=program_name,
|
||||||
|
program_version=httpie.__version__,
|
||||||
|
last_edit_date=httpie.__date__,
|
||||||
|
)
|
||||||
|
builder.set_name(program_name)
|
||||||
|
|
||||||
|
with builder.section('SYNOPSIS'):
|
||||||
|
builder.write(render_as_string(to_usage(spec, program_name=program_name)))
|
||||||
|
|
||||||
|
with builder.section('DESCRIPTION'):
|
||||||
|
builder.write(spec.description)
|
||||||
|
|
||||||
|
for index, group in enumerate(spec.groups, 1):
|
||||||
|
with builder.section(group.name):
|
||||||
|
if group.description:
|
||||||
|
builder.write(group.description)
|
||||||
|
|
||||||
|
for argument in group.arguments:
|
||||||
|
if argument.is_hidden:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw_arg = argument.serialize(isolation_mode=True)
|
||||||
|
|
||||||
|
metavar = raw_arg.get('metavar')
|
||||||
|
if raw_arg.get('is_positional'):
|
||||||
|
# In case of positional arguments, metavar is always equal
|
||||||
|
# to the list of options (e.g `METHOD`).
|
||||||
|
metavar = None
|
||||||
|
builder.add_options(raw_arg['options'], metavar=metavar)
|
||||||
|
|
||||||
|
description = _escape_and_dedent(raw_arg.get('description', ''))
|
||||||
|
description = OPTION_HIGHLIGHT_RE.sub(
|
||||||
|
lambda match: builder.boldify(match['option']),
|
||||||
|
description
|
||||||
|
)
|
||||||
|
builder.write('\n' + description + '\n')
|
||||||
|
|
||||||
|
builder.separate()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
for program_name, spec in [
|
||||||
|
('http', core_options),
|
||||||
|
('https', core_options),
|
||||||
|
('httpie', manager_options),
|
||||||
|
]:
|
||||||
|
with open((MAN_PAGE_PATH / program_name).with_suffix('.1'), 'w') as stream:
|
||||||
|
stream.write(to_man_page(program_name, spec))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -4,5 +4,6 @@ HTTPie: modern, user-friendly command-line HTTP client for the API era.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '3.1.1.dev0'
|
__version__ = '3.1.1.dev0'
|
||||||
|
__date__ = '2022-03-08'
|
||||||
__author__ = 'Jakub Roztocil'
|
__author__ = 'Jakub Roztocil'
|
||||||
__licence__ = 'BSD'
|
__licence__ = 'BSD'
|
||||||
|
@ -155,6 +155,7 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
|||||||
namespace=None
|
namespace=None
|
||||||
) -> argparse.Namespace:
|
) -> argparse.Namespace:
|
||||||
self.env = env
|
self.env = env
|
||||||
|
self.env.args = namespace = namespace or argparse.Namespace()
|
||||||
self.args, no_options = super().parse_known_args(args, namespace)
|
self.args, no_options = super().parse_known_args(args, namespace)
|
||||||
if self.args.debug:
|
if self.args.debug:
|
||||||
self.args.traceback = True
|
self.args.traceback = True
|
||||||
@ -557,19 +558,62 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
|||||||
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
parsed_options = parse_format_options(options_group, defaults=parsed_options)
|
||||||
self.args.format_options = parsed_options
|
self.args.format_options = parsed_options
|
||||||
|
|
||||||
|
def print_manual(self):
|
||||||
|
from httpie.output.ui import man_pages
|
||||||
|
|
||||||
|
if man_pages.is_available(self.env.program_name):
|
||||||
|
man_pages.display_for(self.env, self.env.program_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
text = self.format_help()
|
||||||
|
with self.env.rich_console.pager():
|
||||||
|
self.env.rich_console.print(
|
||||||
|
text,
|
||||||
|
highlight=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def print_help(self):
|
||||||
|
from httpie.output.ui import rich_help
|
||||||
|
|
||||||
|
for renderable in rich_help.to_help_message(self.spec):
|
||||||
|
self.env.rich_console.print(renderable)
|
||||||
|
|
||||||
|
def print_usage(self, file):
|
||||||
|
from rich.text import Text
|
||||||
|
from httpie.output.ui import rich_help
|
||||||
|
|
||||||
|
whitelist = set()
|
||||||
|
_, exception, _ = sys.exc_info()
|
||||||
|
if (
|
||||||
|
isinstance(exception, argparse.ArgumentError)
|
||||||
|
and len(exception.args) >= 1
|
||||||
|
and isinstance(exception.args[0], argparse.Action)
|
||||||
|
and exception.args[0].option_strings
|
||||||
|
):
|
||||||
|
# add_usage path is also taken when you pass an invalid option,
|
||||||
|
# e.g --style=invalid. If something like that happens, we want
|
||||||
|
# to include to action that caused to the invalid usage into
|
||||||
|
# the list of actions we are displaying.
|
||||||
|
whitelist.add(exception.args[0].option_strings[0])
|
||||||
|
|
||||||
|
usage_text = Text('usage', style='bold')
|
||||||
|
usage_text.append(':\n ')
|
||||||
|
usage_text.append(rich_help.to_usage(self.spec, whitelist=whitelist))
|
||||||
|
self.env.rich_error_console.print(usage_text)
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message):
|
||||||
"""Prints a usage message incorporating the message to stderr and
|
"""Prints a usage message incorporating the message to stderr and
|
||||||
exits."""
|
exits."""
|
||||||
self.print_usage(sys.stderr)
|
self.print_usage(sys.stderr)
|
||||||
self.exit(
|
self.env.rich_error_console.print(
|
||||||
2,
|
|
||||||
dedent(
|
dedent(
|
||||||
f'''
|
f'''
|
||||||
error:
|
[bold]error[/bold]:
|
||||||
{message}
|
{message}
|
||||||
|
|
||||||
for more information:
|
[bold]for more information[/bold]:
|
||||||
run '{self.prog} --help' or visit https://httpie.io/docs/cli
|
run '{self.prog} --help' or visit https://httpie.io/docs/cli
|
||||||
'''
|
'''.rstrip()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.exit(2)
|
||||||
|
@ -16,19 +16,23 @@ from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS,
|
|||||||
SORTED_FORMAT_OPTIONS_STRING,
|
SORTED_FORMAT_OPTIONS_STRING,
|
||||||
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
|
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
|
||||||
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
|
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
|
||||||
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE,
|
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE, BUNDLED_STYLES,
|
||||||
get_available_styles)
|
get_available_styles)
|
||||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||||
from httpie.plugins.registry import plugin_manager
|
from httpie.plugins.registry import plugin_manager
|
||||||
from httpie.sessions import DEFAULT_SESSIONS_DIR
|
|
||||||
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
|
||||||
|
|
||||||
options = ParserSpec(
|
options = ParserSpec(
|
||||||
'http',
|
'http',
|
||||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||||
epilog="""
|
epilog="""
|
||||||
|
To learn more, you can try:
|
||||||
|
-> running 'http --manual'
|
||||||
|
-> visiting our full documentation at https://httpie.io/docs/cli
|
||||||
|
|
||||||
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
For every --OPTION there is also a --no-OPTION that reverts OPTION
|
||||||
to its default value.
|
to its default value.
|
||||||
|
|
||||||
Suggestions and bug reports are greatly appreciated:
|
Suggestions and bug reports are greatly appreciated:
|
||||||
https://github.com/httpie/httpie/issues
|
https://github.com/httpie/httpie/issues
|
||||||
""",
|
""",
|
||||||
@ -52,6 +56,7 @@ positional_arguments.add_argument(
|
|||||||
metavar='METHOD',
|
metavar='METHOD',
|
||||||
nargs=Qualifiers.OPTIONAL,
|
nargs=Qualifiers.OPTIONAL,
|
||||||
default=None,
|
default=None,
|
||||||
|
short_help='The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).',
|
||||||
help="""
|
help="""
|
||||||
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
|
||||||
|
|
||||||
@ -66,9 +71,10 @@ positional_arguments.add_argument(
|
|||||||
positional_arguments.add_argument(
|
positional_arguments.add_argument(
|
||||||
dest='url',
|
dest='url',
|
||||||
metavar='URL',
|
metavar='URL',
|
||||||
|
short_help='The request URL.',
|
||||||
help="""
|
help="""
|
||||||
The scheme defaults to 'http://' if the URL does not include one.
|
The request URL. Scheme defaults to 'http://' if the URL
|
||||||
(You can override this with: --default-scheme=https)
|
does not include one. (You can override this with: --default-scheme=http/https)
|
||||||
|
|
||||||
You can also use a shorthand for localhost
|
You can also use a shorthand for localhost
|
||||||
|
|
||||||
@ -83,6 +89,17 @@ positional_arguments.add_argument(
|
|||||||
nargs=Qualifiers.ZERO_OR_MORE,
|
nargs=Qualifiers.ZERO_OR_MORE,
|
||||||
default=None,
|
default=None,
|
||||||
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
|
||||||
|
short_help=(
|
||||||
|
'HTTPie’s request items syntax for specifying HTTP headers, JSON/Form'
|
||||||
|
'data, files, and URL parameters.'
|
||||||
|
),
|
||||||
|
nested_options=[
|
||||||
|
('HTTP Headers', 'Name:Value', 'Arbitrary HTTP header, e.g X-API-Token:123'),
|
||||||
|
('URL Parameters', 'name==value', 'Querystring parameter to the URL, e.g limit==50'),
|
||||||
|
('Data Fields', 'field=value', 'Data fields to be serialized as JSON (default) or Form Data (with --form)'),
|
||||||
|
('Raw JSON Fields', 'field:=json', 'Data field for real JSON types.'),
|
||||||
|
('File upload Fields', 'field@/dir/file', 'Path field for uploading a file.'),
|
||||||
|
],
|
||||||
help=r"""
|
help=r"""
|
||||||
Optional key-value pairs to be included in the request. The separator used
|
Optional key-value pairs to be included in the request. The separator used
|
||||||
determines the type:
|
determines the type:
|
||||||
@ -136,6 +153,7 @@ content_types.add_argument(
|
|||||||
action='store_const',
|
action='store_const',
|
||||||
const=RequestType.JSON,
|
const=RequestType.JSON,
|
||||||
dest='request_type',
|
dest='request_type',
|
||||||
|
short_help='(default) Serialize data items from the command line as a JSON object.',
|
||||||
help="""
|
help="""
|
||||||
(default) Data items from the command line are serialized as a JSON object.
|
(default) Data items from the command line are serialized as a JSON object.
|
||||||
The Content-Type and Accept headers are set to application/json
|
The Content-Type and Accept headers are set to application/json
|
||||||
@ -149,6 +167,7 @@ content_types.add_argument(
|
|||||||
action='store_const',
|
action='store_const',
|
||||||
const=RequestType.FORM,
|
const=RequestType.FORM,
|
||||||
dest='request_type',
|
dest='request_type',
|
||||||
|
short_help='Serialize data items from the command line as form field data.',
|
||||||
help="""
|
help="""
|
||||||
Data items from the command line are serialized as form fields.
|
Data items from the command line are serialized as form fields.
|
||||||
|
|
||||||
@ -163,22 +182,21 @@ content_types.add_argument(
|
|||||||
action='store_const',
|
action='store_const',
|
||||||
const=RequestType.MULTIPART,
|
const=RequestType.MULTIPART,
|
||||||
dest='request_type',
|
dest='request_type',
|
||||||
help="""
|
short_help=(
|
||||||
Similar to --form, but always sends a multipart/form-data
|
'Similar to --form, but always sends a multipart/form-data '
|
||||||
request (i.e., even without files).
|
'request (i.e., even without files).'
|
||||||
|
)
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
content_types.add_argument(
|
content_types.add_argument(
|
||||||
'--boundary',
|
'--boundary',
|
||||||
help="""
|
short_help=(
|
||||||
Specify a custom boundary string for multipart/form-data requests.
|
'Specify a custom boundary string for multipart/form-data requests. '
|
||||||
Only has effect only together with --form.
|
'Only has effect only together with --form.'
|
||||||
|
)
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
content_types.add_argument(
|
content_types.add_argument(
|
||||||
'--raw',
|
'--raw',
|
||||||
|
short_help='Pass raw request data without extra processing.',
|
||||||
help="""
|
help="""
|
||||||
This option allows you to pass raw request data without extra processing
|
This option allows you to pass raw request data without extra processing
|
||||||
(as opposed to the structured request items syntax):
|
(as opposed to the structured request items syntax):
|
||||||
@ -208,6 +226,7 @@ processing_options.add_argument(
|
|||||||
'-x',
|
'-x',
|
||||||
action='count',
|
action='count',
|
||||||
default=0,
|
default=0,
|
||||||
|
short_help='Compress the content with Deflate algorithm.',
|
||||||
help="""
|
help="""
|
||||||
Content compressed (encoded) with Deflate algorithm.
|
Content compressed (encoded) with Deflate algorithm.
|
||||||
The Content-Encoding header is set to deflate.
|
The Content-Encoding header is set to deflate.
|
||||||
@ -223,22 +242,33 @@ processing_options.add_argument(
|
|||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
|
|
||||||
def format_style_help(available_styles):
|
def format_style_help(available_styles, *, isolation_mode: bool = False):
|
||||||
return """
|
text = """
|
||||||
Output coloring style (default is "{default}"). It can be one of:
|
Output coloring style (default is "{default}"). It can be one of:
|
||||||
|
|
||||||
{available_styles}
|
{available_styles}
|
||||||
|
"""
|
||||||
|
if isolation_mode:
|
||||||
|
text += '\n\n'
|
||||||
|
text += 'For finding out all available styles in your system, try:\n\n'
|
||||||
|
text += ' $ http --style\n'
|
||||||
|
text += textwrap.dedent("""
|
||||||
The "{auto_style}" style follows your terminal's ANSI color 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
|
For non-{auto_style} styles to work properly, please make sure that the
|
||||||
$TERM environment variable is set to "xterm-256color" or similar
|
$TERM environment variable is set to "xterm-256color" or similar
|
||||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||||
""".format(
|
""")
|
||||||
default=DEFAULT_STYLE,
|
|
||||||
available_styles='\n'.join(
|
if isolation_mode:
|
||||||
|
available_styles = sorted(BUNDLED_STYLES)
|
||||||
|
|
||||||
|
available_styles_text = '\n'.join(
|
||||||
f' {line.strip()}'
|
f' {line.strip()}'
|
||||||
for line in textwrap.wrap(', '.join(available_styles), 60)
|
for line in textwrap.wrap(', '.join(available_styles), 60)
|
||||||
).strip(),
|
).strip()
|
||||||
|
return text.format(
|
||||||
|
default=DEFAULT_STYLE,
|
||||||
|
available_styles=available_styles_text,
|
||||||
auto_style=AUTO_STYLE,
|
auto_style=AUTO_STYLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -261,6 +291,7 @@ output_processing.add_argument(
|
|||||||
dest='prettify',
|
dest='prettify',
|
||||||
default=PRETTY_STDOUT_TTY_ONLY,
|
default=PRETTY_STDOUT_TTY_ONLY,
|
||||||
choices=sorted(PRETTY_MAP.keys()),
|
choices=sorted(PRETTY_MAP.keys()),
|
||||||
|
short_help='Control the processing of console outputs.',
|
||||||
help="""
|
help="""
|
||||||
Controls output processing. The value can be "none" to not prettify
|
Controls output processing. The value can be "none" to not prettify
|
||||||
the output (default for redirected output), "all" to apply both colors
|
the output (default for redirected output), "all" to apply both colors
|
||||||
@ -276,6 +307,7 @@ output_processing.add_argument(
|
|||||||
default=DEFAULT_STYLE,
|
default=DEFAULT_STYLE,
|
||||||
action='lazy_choices',
|
action='lazy_choices',
|
||||||
getter=get_available_styles,
|
getter=get_available_styles,
|
||||||
|
short_help=f'Output coloring style (default is "{DEFAULT_STYLE}").',
|
||||||
help_formatter=format_style_help,
|
help_formatter=format_style_help,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -291,6 +323,7 @@ output_processing.add_argument(
|
|||||||
output_processing.add_argument(
|
output_processing.add_argument(
|
||||||
'--unsorted',
|
'--unsorted',
|
||||||
**_unsorted_kwargs,
|
**_unsorted_kwargs,
|
||||||
|
short_help='Disables all sorting while formatting output.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Disables all sorting while formatting output. It is a shortcut for:
|
Disables all sorting while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
@ -301,6 +334,7 @@ output_processing.add_argument(
|
|||||||
output_processing.add_argument(
|
output_processing.add_argument(
|
||||||
'--sorted',
|
'--sorted',
|
||||||
**_sorted_kwargs,
|
**_sorted_kwargs,
|
||||||
|
short_help='Re-enables all sorting options while formatting output.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||||
|
|
||||||
@ -312,6 +346,7 @@ output_processing.add_argument(
|
|||||||
'--response-charset',
|
'--response-charset',
|
||||||
metavar='ENCODING',
|
metavar='ENCODING',
|
||||||
type=response_charset_type,
|
type=response_charset_type,
|
||||||
|
short_help='Override the response encoding for terminal display purposes.',
|
||||||
help="""
|
help="""
|
||||||
Override the response encoding for terminal display purposes, e.g.:
|
Override the response encoding for terminal display purposes, e.g.:
|
||||||
|
|
||||||
@ -324,6 +359,7 @@ output_processing.add_argument(
|
|||||||
'--response-mime',
|
'--response-mime',
|
||||||
metavar='MIME_TYPE',
|
metavar='MIME_TYPE',
|
||||||
type=response_mime_type,
|
type=response_mime_type,
|
||||||
|
short_help='Override the response mime type for coloring and formatting for the terminal.',
|
||||||
help="""
|
help="""
|
||||||
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
Override the response mime type for coloring and formatting for the terminal, e.g.:
|
||||||
|
|
||||||
@ -335,6 +371,7 @@ output_processing.add_argument(
|
|||||||
output_processing.add_argument(
|
output_processing.add_argument(
|
||||||
'--format-options',
|
'--format-options',
|
||||||
action='append',
|
action='append',
|
||||||
|
short_help='Controls output formatting.',
|
||||||
help="""
|
help="""
|
||||||
Controls output formatting. Only relevant when formatting is enabled
|
Controls output formatting. Only relevant when formatting is enabled
|
||||||
through (explicit or implied) --pretty=all or --pretty=format.
|
through (explicit or implied) --pretty=all or --pretty=format.
|
||||||
@ -368,6 +405,7 @@ output_options.add_argument(
|
|||||||
'-p',
|
'-p',
|
||||||
dest='output_options',
|
dest='output_options',
|
||||||
metavar='WHAT',
|
metavar='WHAT',
|
||||||
|
short_help='Options to specify what the console output should contain.',
|
||||||
help=f"""
|
help=f"""
|
||||||
String specifying what the output should contain:
|
String specifying what the output should contain:
|
||||||
|
|
||||||
@ -390,6 +428,7 @@ output_options.add_argument(
|
|||||||
dest='output_options',
|
dest='output_options',
|
||||||
action='store_const',
|
action='store_const',
|
||||||
const=OUT_RESP_HEAD,
|
const=OUT_RESP_HEAD,
|
||||||
|
short_help='Print only the response headers.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||||
|
|
||||||
@ -401,6 +440,7 @@ output_options.add_argument(
|
|||||||
dest='output_options',
|
dest='output_options',
|
||||||
action='store_const',
|
action='store_const',
|
||||||
const=OUT_RESP_META,
|
const=OUT_RESP_META,
|
||||||
|
short_help='Print only the response metadata.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
||||||
|
|
||||||
@ -412,6 +452,7 @@ output_options.add_argument(
|
|||||||
dest='output_options',
|
dest='output_options',
|
||||||
action='store_const',
|
action='store_const',
|
||||||
const=OUT_RESP_BODY,
|
const=OUT_RESP_BODY,
|
||||||
|
short_help='Print only the response body.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||||
|
|
||||||
@ -424,20 +465,22 @@ output_options.add_argument(
|
|||||||
dest='verbose',
|
dest='verbose',
|
||||||
action='count',
|
action='count',
|
||||||
default=0,
|
default=0,
|
||||||
|
short_help='Make output more verbose.',
|
||||||
help=f"""
|
help=f"""
|
||||||
Verbose output. For the level one (with single `-v`/`--verbose`), print
|
Verbose output. For the level one (with single `-v`/`--verbose`), print
|
||||||
the whole request as well as the response. Also print any intermediary
|
the whole request as well as the response. Also print any intermediary
|
||||||
requests/responses (such as redirects). For the second level and higher,
|
requests/responses (such as redirects). For the second level and higher,
|
||||||
print these as well as the response metadata.
|
print these as well as the response metadata.
|
||||||
|
|
||||||
Level one is a shortcut for: --all --print={''.join(BASE_OUTPUT_OPTIONS)}
|
Level one is a shortcut for: --all --print={''.join(sorted(BASE_OUTPUT_OPTIONS))}
|
||||||
Level two is a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
Level two is a shortcut for: --all --print={''.join(sorted(OUTPUT_OPTIONS))}
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
output_options.add_argument(
|
output_options.add_argument(
|
||||||
'--all',
|
'--all',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
short_help='Show any intermediary requests/responses.',
|
||||||
help="""
|
help="""
|
||||||
By default, only the final request/response is shown. Use this flag to show
|
By default, only the final request/response is shown. Use this flag to show
|
||||||
any intermediary requests/responses as well. Intermediary requests include
|
any intermediary requests/responses as well. Intermediary requests include
|
||||||
@ -451,6 +494,7 @@ output_options.add_argument(
|
|||||||
'-P',
|
'-P',
|
||||||
dest='output_options_history',
|
dest='output_options_history',
|
||||||
metavar='WHAT',
|
metavar='WHAT',
|
||||||
|
short_help='--print for intermediary requests/responses.',
|
||||||
help="""
|
help="""
|
||||||
The same as --print, -p but applies only to intermediary requests/responses
|
The same as --print, -p but applies only to intermediary requests/responses
|
||||||
(such as redirects) when their inclusion is enabled with --all. If this
|
(such as redirects) when their inclusion is enabled with --all. If this
|
||||||
@ -464,6 +508,7 @@ output_options.add_argument(
|
|||||||
'-S',
|
'-S',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
|
short_help='Always stream the response body by line, i.e., behave like `tail -f`.',
|
||||||
help="""
|
help="""
|
||||||
Always stream the response body by line, i.e., behave like `tail -f'.
|
Always stream the response body by line, i.e., behave like `tail -f'.
|
||||||
|
|
||||||
@ -484,6 +529,7 @@ output_options.add_argument(
|
|||||||
type=FileType('a+b'),
|
type=FileType('a+b'),
|
||||||
dest='output_file',
|
dest='output_file',
|
||||||
metavar='FILE',
|
metavar='FILE',
|
||||||
|
short_help='Save output to FILE instead of stdout.',
|
||||||
help="""
|
help="""
|
||||||
Save output to FILE instead of stdout. If --download is also set, then only
|
Save output to FILE instead of stdout. If --download is also set, then only
|
||||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||||
@ -497,6 +543,7 @@ output_options.add_argument(
|
|||||||
'-d',
|
'-d',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
|
short_help='Download the body to a file instead of printing it to stdout.',
|
||||||
help="""
|
help="""
|
||||||
Do not print the response body to stdout. Rather, download it and store it
|
Do not print the response body to stdout. Rather, download it and store it
|
||||||
in a file. The filename is guessed unless specified with --output
|
in a file. The filename is guessed unless specified with --output
|
||||||
@ -510,6 +557,7 @@ output_options.add_argument(
|
|||||||
dest='download_resume',
|
dest='download_resume',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
|
short_help='Resume an interrupted download (--output needs to be specified).',
|
||||||
help="""
|
help="""
|
||||||
Resume an interrupted download. Note that the --output option needs to be
|
Resume an interrupted download. Note that the --output option needs to be
|
||||||
specified as well.
|
specified as well.
|
||||||
@ -521,6 +569,7 @@ output_options.add_argument(
|
|||||||
'-q',
|
'-q',
|
||||||
action='count',
|
action='count',
|
||||||
default=0,
|
default=0,
|
||||||
|
short_help='Do not print to stdout or stderr, except for errors and warnings when provided once.',
|
||||||
help="""
|
help="""
|
||||||
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||||
Provide twice to suppress warnings as well.
|
Provide twice to suppress warnings as well.
|
||||||
@ -544,21 +593,26 @@ sessions.add_argument(
|
|||||||
'--session',
|
'--session',
|
||||||
metavar='SESSION_NAME_OR_PATH',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
type=session_name_validator,
|
type=session_name_validator,
|
||||||
help=f"""
|
short_help='Create, or reuse and update a session.',
|
||||||
|
help="""
|
||||||
Create, or reuse and update a session. Within a session, custom headers,
|
Create, or reuse and update a session. Within a session, custom headers,
|
||||||
auth credential, as well as any cookies sent by the server persist between
|
auth credential, as well as any cookies sent by the server persist between
|
||||||
requests.
|
requests.
|
||||||
|
|
||||||
Session files are stored in:
|
Session files are stored in:
|
||||||
|
|
||||||
{DEFAULT_SESSIONS_DIR}/<HOST>/<SESSION_NAME>.json.
|
[HTTPIE_CONFIG_DIR]/<HOST>/<SESSION_NAME>.json.
|
||||||
|
|
||||||
|
See the following page to find out your default HTTPIE_CONFIG_DIR:
|
||||||
|
|
||||||
|
https://httpie.io/docs/cli/config-file-directory
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
sessions.add_argument(
|
sessions.add_argument(
|
||||||
'--session-read-only',
|
'--session-read-only',
|
||||||
metavar='SESSION_NAME_OR_PATH',
|
metavar='SESSION_NAME_OR_PATH',
|
||||||
type=session_name_validator,
|
type=session_name_validator,
|
||||||
|
short_help='Create or read a session without updating it',
|
||||||
help="""
|
help="""
|
||||||
Create or read a session without updating it form the request/response
|
Create or read a session without updating it form the request/response
|
||||||
exchange.
|
exchange.
|
||||||
@ -571,16 +625,25 @@ sessions.add_argument(
|
|||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
|
|
||||||
def format_auth_help(auth_plugins_mapping):
|
def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
|
||||||
auth_plugins = list(auth_plugins_mapping.values())
|
text = """
|
||||||
return """
|
|
||||||
The authentication mechanism to be used. Defaults to "{default}".
|
The authentication mechanism to be used. Defaults to "{default}".
|
||||||
|
|
||||||
{types}
|
{auth_types}
|
||||||
|
"""
|
||||||
|
|
||||||
""".format(
|
auth_plugins = list(auth_plugins_mapping.values())
|
||||||
default=auth_plugins[0].auth_type,
|
if isolation_mode:
|
||||||
types='\n '.join(
|
auth_plugins = [
|
||||||
|
auth_plugin
|
||||||
|
for auth_plugin in auth_plugins
|
||||||
|
if issubclass(auth_plugin, BuiltinAuthPlugin)
|
||||||
|
]
|
||||||
|
text += '\n'
|
||||||
|
text += 'For finding out all available authentication types in your system, try:\n\n'
|
||||||
|
text += ' $ http --auth-type'
|
||||||
|
|
||||||
|
auth_types = '\n\n '.join(
|
||||||
'"{type}": {name}{package}{description}'.format(
|
'"{type}": {name}{package}{description}'.format(
|
||||||
type=plugin.auth_type,
|
type=plugin.auth_type,
|
||||||
name=plugin.name,
|
name=plugin.name,
|
||||||
@ -597,7 +660,11 @@ def format_auth_help(auth_plugins_mapping):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
for plugin in auth_plugins
|
for plugin in auth_plugins
|
||||||
),
|
)
|
||||||
|
|
||||||
|
return text.format(
|
||||||
|
default=auth_plugins[0].auth_type,
|
||||||
|
auth_types=auth_types,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -608,6 +675,7 @@ authentication.add_argument(
|
|||||||
'-a',
|
'-a',
|
||||||
default=None,
|
default=None,
|
||||||
metavar='USER[:PASS] | TOKEN',
|
metavar='USER[:PASS] | TOKEN',
|
||||||
|
short_help='Credentials for the selected (-A) authentication method.',
|
||||||
help="""
|
help="""
|
||||||
For username/password based authentication mechanisms (e.g
|
For username/password based authentication mechanisms (e.g
|
||||||
basic auth or digest auth) if only the username is provided
|
basic auth or digest auth) if only the username is provided
|
||||||
@ -623,16 +691,14 @@ authentication.add_argument(
|
|||||||
getter=plugin_manager.get_auth_plugin_mapping,
|
getter=plugin_manager.get_auth_plugin_mapping,
|
||||||
sort=True,
|
sort=True,
|
||||||
cache=False,
|
cache=False,
|
||||||
|
short_help='The authentication mechanism to be used.',
|
||||||
help_formatter=format_auth_help,
|
help_formatter=format_auth_help,
|
||||||
)
|
)
|
||||||
authentication.add_argument(
|
authentication.add_argument(
|
||||||
'--ignore-netrc',
|
'--ignore-netrc',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="""
|
short_help='Ignore credentials from .netrc.'
|
||||||
Ignore credentials from .netrc.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
@ -645,9 +711,7 @@ network.add_argument(
|
|||||||
'--offline',
|
'--offline',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="""
|
short_help='Build the request and print it but don’t actually send it.'
|
||||||
Build the request and print it but don’t actually send it.
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
network.add_argument(
|
network.add_argument(
|
||||||
'--proxy',
|
'--proxy',
|
||||||
@ -655,6 +719,7 @@ network.add_argument(
|
|||||||
action='append',
|
action='append',
|
||||||
metavar='PROTOCOL:PROXY_URL',
|
metavar='PROTOCOL:PROXY_URL',
|
||||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||||
|
short_help='String mapping of protocol to the URL of the proxy.',
|
||||||
help="""
|
help="""
|
||||||
String mapping protocol to the URL of the proxy
|
String mapping protocol to the URL of the proxy
|
||||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||||
@ -668,16 +733,14 @@ network.add_argument(
|
|||||||
'-F',
|
'-F',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="""
|
short_help='Follow 30x Location redirects.'
|
||||||
Follow 30x Location redirects.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
network.add_argument(
|
network.add_argument(
|
||||||
'--max-redirects',
|
'--max-redirects',
|
||||||
type=int,
|
type=int,
|
||||||
default=30,
|
default=30,
|
||||||
|
short_help='The maximum number of redirects that should be followed (with --follow).',
|
||||||
help="""
|
help="""
|
||||||
By default, requests have a limit of 30 redirects (works with --follow).
|
By default, requests have a limit of 30 redirects (works with --follow).
|
||||||
|
|
||||||
@ -687,11 +750,10 @@ network.add_argument(
|
|||||||
'--max-headers',
|
'--max-headers',
|
||||||
type=int,
|
type=int,
|
||||||
default=0,
|
default=0,
|
||||||
help="""
|
short_help=(
|
||||||
The maximum number of response headers to be read before giving up
|
'The maximum number of response headers to be read before '
|
||||||
(default 0, i.e., no limit).
|
'giving up (default 0, i.e., no limit).'
|
||||||
|
)
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
network.add_argument(
|
network.add_argument(
|
||||||
@ -699,6 +761,7 @@ network.add_argument(
|
|||||||
type=float,
|
type=float,
|
||||||
default=0,
|
default=0,
|
||||||
metavar='SECONDS',
|
metavar='SECONDS',
|
||||||
|
short_help='The connection timeout of the request in seconds.',
|
||||||
help="""
|
help="""
|
||||||
The connection timeout of the request in seconds.
|
The connection timeout of the request in seconds.
|
||||||
The default value is 0, i.e., there is no timeout limit.
|
The default value is 0, i.e., there is no timeout limit.
|
||||||
@ -713,6 +776,7 @@ network.add_argument(
|
|||||||
'--check-status',
|
'--check-status',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
short_help='Exit with an error status code if the server replies with an error.',
|
||||||
help="""
|
help="""
|
||||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||||
@ -729,20 +793,16 @@ network.add_argument(
|
|||||||
'--path-as-is',
|
'--path-as-is',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="""
|
short_help='Bypass dot segment (/../ or /./) URL squashing.'
|
||||||
Bypass dot segment (/../ or /./) URL squashing.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
network.add_argument(
|
network.add_argument(
|
||||||
'--chunked',
|
'--chunked',
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="""
|
short_help=(
|
||||||
Enable streaming via chunked transfer encoding.
|
'Enable streaming via chunked transfer encoding. '
|
||||||
The Transfer-Encoding header is set to chunked.
|
'The Transfer-Encoding header is set to chunked.'
|
||||||
|
)
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
@ -754,6 +814,7 @@ ssl = options.add_group('SSL')
|
|||||||
ssl.add_argument(
|
ssl.add_argument(
|
||||||
'--verify',
|
'--verify',
|
||||||
default='yes',
|
default='yes',
|
||||||
|
short_help='If "no", skip SSL verification. If a file path, use it as a CA bundle.',
|
||||||
help="""
|
help="""
|
||||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||||
@ -765,6 +826,7 @@ ssl.add_argument(
|
|||||||
'--ssl',
|
'--ssl',
|
||||||
dest='ssl_version',
|
dest='ssl_version',
|
||||||
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
|
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
|
||||||
|
short_help='The desired protocol version to used.',
|
||||||
help="""
|
help="""
|
||||||
The desired protocol version to use. This will default to
|
The desired protocol version to use. This will default to
|
||||||
SSL v2.3 which will negotiate the highest protocol that both
|
SSL v2.3 which will negotiate the highest protocol that both
|
||||||
@ -776,6 +838,7 @@ ssl.add_argument(
|
|||||||
)
|
)
|
||||||
ssl.add_argument(
|
ssl.add_argument(
|
||||||
'--ciphers',
|
'--ciphers',
|
||||||
|
short_help='A string in the OpenSSL cipher list format.',
|
||||||
help=f"""
|
help=f"""
|
||||||
|
|
||||||
A string in the OpenSSL cipher list format. By default, the following
|
A string in the OpenSSL cipher list format. By default, the following
|
||||||
@ -789,6 +852,7 @@ ssl.add_argument(
|
|||||||
'--cert',
|
'--cert',
|
||||||
default=None,
|
default=None,
|
||||||
type=readable_file_arg,
|
type=readable_file_arg,
|
||||||
|
short_help='Specifys a local cert to use as client side SSL certificate.',
|
||||||
help="""
|
help="""
|
||||||
You can specify a local cert to use as client side SSL certificate.
|
You can specify a local cert to use as client side SSL certificate.
|
||||||
This file may either contain both private key and certificate or you may
|
This file may either contain both private key and certificate or you may
|
||||||
@ -800,6 +864,7 @@ ssl.add_argument(
|
|||||||
'--cert-key',
|
'--cert-key',
|
||||||
default=None,
|
default=None,
|
||||||
type=readable_file_arg,
|
type=readable_file_arg,
|
||||||
|
short_help='The private key to use with SSL. Only needed if --cert is given.',
|
||||||
help="""
|
help="""
|
||||||
The private key to use with SSL. Only needed if --cert is given and the
|
The private key to use with SSL. Only needed if --cert is given and the
|
||||||
certificate file does not contain the private key.
|
certificate file does not contain the private key.
|
||||||
@ -811,11 +876,12 @@ ssl.add_argument(
|
|||||||
'--cert-key-pass',
|
'--cert-key-pass',
|
||||||
default=None,
|
default=None,
|
||||||
type=SSLCredentials,
|
type=SSLCredentials,
|
||||||
help='''
|
short_help='The passphrase to be used to with the given private key.',
|
||||||
|
help="""
|
||||||
The passphrase to be used to with the given private key. Only needed if --cert-key
|
The passphrase to be used to with the given private key. Only needed if --cert-key
|
||||||
is given and the key file requires a passphrase.
|
is given and the key file requires a passphrase.
|
||||||
If not provided, you’ll be prompted interactively.
|
If not provided, you’ll be prompted interactively.
|
||||||
'''
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
@ -828,50 +894,42 @@ troubleshooting.add_argument(
|
|||||||
'-I',
|
'-I',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help="""
|
short_help='Do not attempt to read stdin'
|
||||||
Do not attempt to read stdin.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
troubleshooting.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--help',
|
'--help',
|
||||||
action='help',
|
action='help',
|
||||||
default=Qualifiers.SUPPRESS,
|
default=Qualifiers.SUPPRESS,
|
||||||
help="""
|
short_help='Show this help message and exit.',
|
||||||
Show this help message and exit.
|
)
|
||||||
|
troubleshooting.add_argument(
|
||||||
""",
|
'--manual',
|
||||||
|
action='manual',
|
||||||
|
default=Qualifiers.SUPPRESS,
|
||||||
|
short_help='Show the full manual.',
|
||||||
)
|
)
|
||||||
troubleshooting.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
version=__version__,
|
version=__version__,
|
||||||
help="""
|
short_help='Show version and exit.',
|
||||||
Show version and exit.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
troubleshooting.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--traceback',
|
'--traceback',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
help="""
|
short_help='Prints the exception traceback should one occur.',
|
||||||
Prints the exception traceback should one occur.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
troubleshooting.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--default-scheme',
|
'--default-scheme',
|
||||||
default='http',
|
default='http',
|
||||||
help="""
|
short_help='The default scheme to use if not specified in the URL.'
|
||||||
The default scheme to use if not specified in the URL.
|
|
||||||
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
troubleshooting.add_argument(
|
troubleshooting.add_argument(
|
||||||
'--debug',
|
'--debug',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
default=False,
|
default=False,
|
||||||
|
short_help='Print useful diagnostic information for bug reports.',
|
||||||
help="""
|
help="""
|
||||||
Prints the exception traceback should one occur, as well as other
|
Prints the exception traceback should one occur, as well as other
|
||||||
information useful for debugging HTTPie itself and for reporting bugs.
|
information useful for debugging HTTPie itself and for reporting bugs.
|
||||||
|
@ -3,15 +3,16 @@ import textwrap
|
|||||||
import typing
|
import typing
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Any, Optional, Dict, List, Type, TypeVar
|
from typing import Any, Optional, Dict, List, Tuple, Type, TypeVar
|
||||||
|
|
||||||
from httpie.cli.argparser import HTTPieArgumentParser
|
from httpie.cli.argparser import HTTPieArgumentParser
|
||||||
from httpie.cli.utils import LazyChoices
|
from httpie.cli.utils import Manual, LazyChoices
|
||||||
|
|
||||||
|
|
||||||
class Qualifiers(Enum):
|
class Qualifiers(Enum):
|
||||||
OPTIONAL = auto()
|
OPTIONAL = auto()
|
||||||
ZERO_OR_MORE = auto()
|
ZERO_OR_MORE = auto()
|
||||||
|
ONE_OR_MORE = auto()
|
||||||
SUPPRESS = auto()
|
SUPPRESS = auto()
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +25,27 @@ def map_qualifiers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def drop_keys(
|
||||||
|
configuration: Dict[str, Any], key_blacklist: Tuple[str, ...]
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
key: value
|
||||||
|
for key, value in configuration.items()
|
||||||
|
if key not in key_blacklist
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_first_line(source: str) -> str:
|
||||||
|
parts = []
|
||||||
|
for line in source.strip().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
parts.append(line)
|
||||||
|
if line.endswith("."):
|
||||||
|
break
|
||||||
|
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
PARSER_SPEC_VERSION = '0.0.1a0'
|
PARSER_SPEC_VERSION = '0.0.1a0'
|
||||||
|
|
||||||
|
|
||||||
@ -69,6 +91,7 @@ class Group:
|
|||||||
|
|
||||||
def add_argument(self, *args, **kwargs):
|
def add_argument(self, *args, **kwargs):
|
||||||
argument = Argument(list(args), kwargs.copy())
|
argument = Argument(list(args), kwargs.copy())
|
||||||
|
argument.post_init()
|
||||||
self.arguments.append(argument)
|
self.arguments.append(argument)
|
||||||
return argument
|
return argument
|
||||||
|
|
||||||
@ -85,14 +108,32 @@ class Argument(typing.NamedTuple):
|
|||||||
aliases: List[str]
|
aliases: List[str]
|
||||||
configuration: Dict[str, Any]
|
configuration: Dict[str, Any]
|
||||||
|
|
||||||
def serialize(self) -> Dict[str, Any]:
|
def post_init(self):
|
||||||
|
"""Run a bunch of post-init hooks."""
|
||||||
|
# If there is a short help, then create the longer version from it.
|
||||||
|
short_help = self.configuration.get('short_help')
|
||||||
|
if (
|
||||||
|
short_help
|
||||||
|
and 'help' not in self.configuration
|
||||||
|
and self.configuration.get('action') != 'lazy_choices'
|
||||||
|
):
|
||||||
|
self.configuration['help'] = f'\n{short_help}\n\n'
|
||||||
|
|
||||||
|
def serialize(self, *, isolation_mode: bool = False) -> Dict[str, Any]:
|
||||||
configuration = self.configuration.copy()
|
configuration = self.configuration.copy()
|
||||||
|
|
||||||
# Unpack the dynamically computed choices, since we
|
# Unpack the dynamically computed choices, since we
|
||||||
# will need to store the actual values somewhere.
|
# will need to store the actual values somewhere.
|
||||||
action = configuration.pop('action', None)
|
action = configuration.pop('action', None)
|
||||||
|
short_help = configuration.pop('short_help', None)
|
||||||
|
nested_options = configuration.pop('nested_options', None)
|
||||||
|
|
||||||
if action == 'lazy_choices':
|
if action == 'lazy_choices':
|
||||||
choices = LazyChoices(self.aliases, **{'dest': None, **configuration})
|
choices = LazyChoices(
|
||||||
|
self.aliases,
|
||||||
|
**{'dest': None, **configuration},
|
||||||
|
isolation_mode=isolation_mode
|
||||||
|
)
|
||||||
configuration['choices'] = list(choices.load())
|
configuration['choices'] = list(choices.load())
|
||||||
configuration['help'] = choices.help
|
configuration['help'] = choices.help
|
||||||
|
|
||||||
@ -106,9 +147,13 @@ class Argument(typing.NamedTuple):
|
|||||||
qualifiers = JSON_QUALIFIER_TO_OPTIONS[configuration.get('nargs', Qualifiers.SUPPRESS)]
|
qualifiers = JSON_QUALIFIER_TO_OPTIONS[configuration.get('nargs', Qualifiers.SUPPRESS)]
|
||||||
result.update(qualifiers)
|
result.update(qualifiers)
|
||||||
|
|
||||||
help_msg = configuration.get('help')
|
description = configuration.get('help')
|
||||||
if help_msg and help_msg is not Qualifiers.SUPPRESS:
|
if description and description is not Qualifiers.SUPPRESS:
|
||||||
result['description'] = help_msg.strip()
|
result['short_description'] = short_help
|
||||||
|
result['description'] = description
|
||||||
|
|
||||||
|
if nested_options:
|
||||||
|
result['nested_options'] = nested_options
|
||||||
|
|
||||||
python_type = configuration.get('type')
|
python_type = configuration.get('type')
|
||||||
if python_type is not None:
|
if python_type is not None:
|
||||||
@ -123,10 +168,19 @@ class Argument(typing.NamedTuple):
|
|||||||
key: value
|
key: value
|
||||||
for key, value in configuration.items()
|
for key, value in configuration.items()
|
||||||
if key in JSON_DIRECT_MIRROR_OPTIONS
|
if key in JSON_DIRECT_MIRROR_OPTIONS
|
||||||
|
if value is not Qualifiers.SUPPRESS
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_positional(self):
|
||||||
|
return len(self.aliases) == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hidden(self):
|
||||||
|
return self.configuration.get('help') is Qualifiers.SUPPRESS
|
||||||
|
|
||||||
def __getattr__(self, attribute_name):
|
def __getattr__(self, attribute_name):
|
||||||
if attribute_name in self.configuration:
|
if attribute_name in self.configuration:
|
||||||
return self.configuration[attribute_name]
|
return self.configuration[attribute_name]
|
||||||
@ -140,7 +194,9 @@ ARGPARSE_QUALIFIER_MAP = {
|
|||||||
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
||||||
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
||||||
Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE,
|
Qualifiers.ZERO_OR_MORE: argparse.ZERO_OR_MORE,
|
||||||
|
Qualifiers.ONE_OR_MORE: argparse.ONE_OR_MORE
|
||||||
}
|
}
|
||||||
|
ARGPARSE_IGNORE_KEYS = ('short_help', 'nested_options')
|
||||||
|
|
||||||
|
|
||||||
def to_argparse(
|
def to_argparse(
|
||||||
@ -152,7 +208,9 @@ def to_argparse(
|
|||||||
description=abstract_options.description,
|
description=abstract_options.description,
|
||||||
epilog=abstract_options.epilog,
|
epilog=abstract_options.epilog,
|
||||||
)
|
)
|
||||||
|
concrete_parser.spec = abstract_options
|
||||||
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
||||||
|
concrete_parser.register('action', 'manual', Manual)
|
||||||
|
|
||||||
for abstract_group in abstract_options.groups:
|
for abstract_group in abstract_options.groups:
|
||||||
concrete_group = concrete_parser.add_argument_group(
|
concrete_group = concrete_parser.add_argument_group(
|
||||||
@ -164,9 +222,9 @@ def to_argparse(
|
|||||||
for abstract_argument in abstract_group.arguments:
|
for abstract_argument in abstract_group.arguments:
|
||||||
concrete_group.add_argument(
|
concrete_group.add_argument(
|
||||||
*abstract_argument.aliases,
|
*abstract_argument.aliases,
|
||||||
**map_qualifiers(
|
**drop_keys(map_qualifiers(
|
||||||
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
||||||
)
|
), ARGPARSE_IGNORE_KEYS)
|
||||||
)
|
)
|
||||||
|
|
||||||
return concrete_parser
|
return concrete_parser
|
||||||
@ -181,9 +239,19 @@ JSON_DIRECT_MIRROR_OPTIONS = (
|
|||||||
JSON_QUALIFIER_TO_OPTIONS = {
|
JSON_QUALIFIER_TO_OPTIONS = {
|
||||||
Qualifiers.OPTIONAL: {'is_optional': True},
|
Qualifiers.OPTIONAL: {'is_optional': True},
|
||||||
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
||||||
|
Qualifiers.ONE_OR_MORE: {'is_optional': False, 'is_variadic': True},
|
||||||
Qualifiers.SUPPRESS: {}
|
Qualifiers.SUPPRESS: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
||||||
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
|
return {'version': PARSER_SPEC_VERSION, 'spec': abstract_options.serialize()}
|
||||||
|
|
||||||
|
|
||||||
|
def parser_to_parser_spec(parser: argparse.ArgumentParser) -> ParserSpec:
|
||||||
|
"""Take an existing argparse parser, and create a spec from it."""
|
||||||
|
return ParserSpec(
|
||||||
|
program=parser.prog,
|
||||||
|
description=parser.description,
|
||||||
|
epilog=parser.epilog
|
||||||
|
)
|
||||||
|
@ -4,20 +4,43 @@ from typing import Any, Callable, Generic, Iterator, Iterable, Optional, TypeVar
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class Manual(argparse.Action):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
option_strings,
|
||||||
|
dest=argparse.SUPPRESS,
|
||||||
|
default=argparse.SUPPRESS,
|
||||||
|
help=None
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
option_strings=option_strings,
|
||||||
|
dest=dest,
|
||||||
|
default=default,
|
||||||
|
nargs=0,
|
||||||
|
help=help
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
parser.print_manual()
|
||||||
|
parser.exit()
|
||||||
|
|
||||||
|
|
||||||
class LazyChoices(argparse.Action, Generic[T]):
|
class LazyChoices(argparse.Action, Generic[T]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
getter: Callable[[], Iterable[T]],
|
getter: Callable[[], Iterable[T]],
|
||||||
help_formatter: Optional[Callable[[T], str]] = None,
|
help_formatter: Optional[Callable[[T, bool], str]] = None,
|
||||||
sort: bool = False,
|
sort: bool = False,
|
||||||
cache: bool = True,
|
cache: bool = True,
|
||||||
|
isolation_mode: bool = False,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> None:
|
) -> None:
|
||||||
self.getter = getter
|
self.getter = getter
|
||||||
self.help_formatter = help_formatter
|
self.help_formatter = help_formatter
|
||||||
self.sort = sort
|
self.sort = sort
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
self.isolation_mode = isolation_mode
|
||||||
self._help: Optional[str] = None
|
self._help: Optional[str] = None
|
||||||
self._obj: Optional[Iterable[T]] = None
|
self._obj: Optional[Iterable[T]] = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -33,7 +56,10 @@ class LazyChoices(argparse.Action, Generic[T]):
|
|||||||
@property
|
@property
|
||||||
def help(self) -> str:
|
def help(self) -> str:
|
||||||
if self._help is None and self.help_formatter is not None:
|
if self._help is None and self.help_formatter is not None:
|
||||||
self._help = self.help_formatter(self.load())
|
self._help = self.help_formatter(
|
||||||
|
self.load(),
|
||||||
|
isolation_mode=self.isolation_mode
|
||||||
|
)
|
||||||
return self._help
|
return self._help
|
||||||
|
|
||||||
@help.setter
|
@help.setter
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, IO, Optional
|
from typing import Iterator, IO, Optional, TYPE_CHECKING
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
@ -12,11 +13,15 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
curses = None # Compiled w/o curses
|
curses = None # Compiled w/o curses
|
||||||
|
|
||||||
from .compat import is_windows
|
from .compat import is_windows, cached_property
|
||||||
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
||||||
from .encoding import UTF8
|
from .encoding import UTF8
|
||||||
|
|
||||||
from .utils import repr_dict
|
from .utils import repr_dict
|
||||||
|
from httpie.output.ui import rich_palette as palette
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
|
||||||
class Levels(str, Enum):
|
class Levels(str, Enum):
|
||||||
@ -40,6 +45,7 @@ class Environment:
|
|||||||
is used by the test suite to simulate various scenarios.
|
is used by the test suite to simulate various scenarios.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
args = argparse.Namespace()
|
||||||
is_windows: bool = is_windows
|
is_windows: bool = is_windows
|
||||||
config_dir: Path = DEFAULT_CONFIG_DIR
|
config_dir: Path = DEFAULT_CONFIG_DIR
|
||||||
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
||||||
@ -52,6 +58,10 @@ class Environment:
|
|||||||
stderr_isatty: bool = stderr.isatty()
|
stderr_isatty: bool = stderr.isatty()
|
||||||
colors = 256
|
colors = 256
|
||||||
program_name: str = 'http'
|
program_name: str = 'http'
|
||||||
|
|
||||||
|
# Whether to show progress bars / status spinners etc.
|
||||||
|
show_displays: bool = True
|
||||||
|
|
||||||
if not is_windows:
|
if not is_windows:
|
||||||
if curses:
|
if curses:
|
||||||
try:
|
try:
|
||||||
@ -160,3 +170,49 @@ class Environment:
|
|||||||
def apply_warnings_filter(self) -> None:
|
def apply_warnings_filter(self) -> None:
|
||||||
if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]:
|
if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]:
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
|
|
||||||
|
def _make_rich_console(
|
||||||
|
self,
|
||||||
|
file: IO[str],
|
||||||
|
force_terminal: bool
|
||||||
|
) -> 'Console':
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.theme import Theme
|
||||||
|
from rich.style import Style
|
||||||
|
|
||||||
|
style = getattr(self.args, 'style', palette.AUTO_STYLE)
|
||||||
|
theme = {}
|
||||||
|
if style in palette.STYLE_SHADES:
|
||||||
|
shade = palette.STYLE_SHADES[style]
|
||||||
|
theme.update({
|
||||||
|
color: Style(
|
||||||
|
color=palette.get_color(
|
||||||
|
color,
|
||||||
|
shade,
|
||||||
|
palette=palette.RICH_THEME_PALETTE
|
||||||
|
),
|
||||||
|
bold=True
|
||||||
|
)
|
||||||
|
for color in palette.RICH_THEME_PALETTE
|
||||||
|
})
|
||||||
|
|
||||||
|
# Rich infers the rest of the knowledge (e.g encoding)
|
||||||
|
# dynamically by looking at the file/stderr.
|
||||||
|
return Console(
|
||||||
|
file=file,
|
||||||
|
force_terminal=force_terminal,
|
||||||
|
no_color=(self.colors == 0),
|
||||||
|
theme=Theme(theme)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rich recommends separting the actual console (stdout) from
|
||||||
|
# the error (stderr) console for better isolation between parts.
|
||||||
|
# https://rich.readthedocs.io/en/stable/console.html#error-console
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def rich_console(self):
|
||||||
|
return self._make_rich_console(self.stdout, self.stdout_isatty)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def rich_error_console(self):
|
||||||
|
return self._make_rich_console(self.stderr, self.stderr_isatty)
|
||||||
|
@ -195,7 +195,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
|
|||||||
try:
|
try:
|
||||||
if args.download:
|
if args.download:
|
||||||
args.follow = True # --download implies --follow.
|
args.follow = True # --download implies --follow.
|
||||||
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
|
||||||
downloader.pre_request(args.headers)
|
downloader.pre_request(args.headers)
|
||||||
messages = collect_messages(env, args=args,
|
messages = collect_messages(env, args=args,
|
||||||
request_body_read_callback=request_body_read_callback)
|
request_body_read_callback=request_body_read_callback)
|
||||||
|
@ -5,10 +5,8 @@ Download mode implementation.
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
from mailbox import Message
|
from mailbox import Message
|
||||||
from time import sleep, monotonic
|
from time import monotonic
|
||||||
from typing import IO, Optional, Tuple
|
from typing import IO, Optional, Tuple
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
@ -16,22 +14,11 @@ import requests
|
|||||||
|
|
||||||
from .models import HTTPResponse, OutputOptions
|
from .models import HTTPResponse, OutputOptions
|
||||||
from .output.streams import RawStream
|
from .output.streams import RawStream
|
||||||
from .utils import humanize_bytes
|
from .context import Environment
|
||||||
|
|
||||||
|
|
||||||
PARTIAL_CONTENT = 206
|
PARTIAL_CONTENT = 206
|
||||||
|
|
||||||
CLEAR_LINE = '\r\033[K'
|
|
||||||
PROGRESS = (
|
|
||||||
'{percentage: 6.2f} %'
|
|
||||||
' {downloaded: >10}'
|
|
||||||
' {speed: >10}/s'
|
|
||||||
' {eta: >8} ETA'
|
|
||||||
)
|
|
||||||
PROGRESS_NO_CONTENT_LENGTH = '{downloaded: >10} {speed: >10}/s'
|
|
||||||
SUMMARY = 'Done. {downloaded} in {time:0.5f}s ({speed}/s)\n'
|
|
||||||
SPINNER = '|/-\\'
|
|
||||||
|
|
||||||
|
|
||||||
class ContentRangeError(ValueError):
|
class ContentRangeError(ValueError):
|
||||||
pass
|
pass
|
||||||
@ -176,9 +163,9 @@ class Downloader:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
env: Environment,
|
||||||
output_file: IO = None,
|
output_file: IO = None,
|
||||||
resume: bool = False,
|
resume: bool = False
|
||||||
progress_file: IO = sys.stderr
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param resume: Should the download resume if partial download
|
:param resume: Should the download resume if partial download
|
||||||
@ -191,14 +178,10 @@ class Downloader:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.finished = False
|
self.finished = False
|
||||||
self.status = DownloadStatus()
|
self.status = DownloadStatus(env=env)
|
||||||
self._output_file = output_file
|
self._output_file = output_file
|
||||||
self._resume = resume
|
self._resume = resume
|
||||||
self._resumed_from = 0
|
self._resumed_from = 0
|
||||||
self._progress_reporter = ProgressReporterThread(
|
|
||||||
status=self.status,
|
|
||||||
output=progress_file
|
|
||||||
)
|
|
||||||
|
|
||||||
def pre_request(self, request_headers: dict):
|
def pre_request(self, request_headers: dict):
|
||||||
"""Called just before the HTTP request is sent.
|
"""Called just before the HTTP request is sent.
|
||||||
@ -261,11 +244,6 @@ class Downloader:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass # stdout
|
pass # stdout
|
||||||
|
|
||||||
self.status.started(
|
|
||||||
resumed_from=self._resumed_from,
|
|
||||||
total_size=total_size
|
|
||||||
)
|
|
||||||
|
|
||||||
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
output_options = OutputOptions.from_message(final_response, headers=False, body=True)
|
||||||
stream = RawStream(
|
stream = RawStream(
|
||||||
msg=HTTPResponse(final_response),
|
msg=HTTPResponse(final_response),
|
||||||
@ -273,11 +251,11 @@ class Downloader:
|
|||||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._progress_reporter.output.write(
|
self.status.started(
|
||||||
f'Downloading {humanize_bytes(total_size) + " " if total_size is not None else ""}'
|
output_file=self._output_file,
|
||||||
f'to "{self._output_file.name}"\n'
|
resumed_from=self._resumed_from,
|
||||||
|
total_size=total_size
|
||||||
)
|
)
|
||||||
self._progress_reporter.start()
|
|
||||||
|
|
||||||
return stream, self._output_file
|
return stream, self._output_file
|
||||||
|
|
||||||
@ -287,7 +265,7 @@ class Downloader:
|
|||||||
self.status.finished()
|
self.status.finished()
|
||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
self._progress_reporter.stop()
|
self.status.terminate()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def interrupted(self) -> bool:
|
def interrupted(self) -> bool:
|
||||||
@ -329,127 +307,71 @@ class Downloader:
|
|||||||
class DownloadStatus:
|
class DownloadStatus:
|
||||||
"""Holds details about the download status."""
|
"""Holds details about the download status."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, env):
|
||||||
|
self.env = env
|
||||||
self.downloaded = 0
|
self.downloaded = 0
|
||||||
self.total_size = None
|
self.total_size = None
|
||||||
self.resumed_from = 0
|
self.resumed_from = 0
|
||||||
self.time_started = None
|
self.time_started = None
|
||||||
self.time_finished = None
|
self.time_finished = None
|
||||||
|
|
||||||
def started(self, resumed_from=0, total_size=None):
|
def started(self, output_file, resumed_from=0, total_size=None):
|
||||||
assert self.time_started is None
|
assert self.time_started is None
|
||||||
self.total_size = total_size
|
self.total_size = total_size
|
||||||
self.downloaded = self.resumed_from = resumed_from
|
self.downloaded = self.resumed_from = resumed_from
|
||||||
self.time_started = monotonic()
|
self.time_started = monotonic()
|
||||||
|
self.start_display(output_file=output_file)
|
||||||
|
|
||||||
|
def start_display(self, output_file):
|
||||||
|
from httpie.output.ui.rich_progress import (
|
||||||
|
DummyDisplay,
|
||||||
|
StatusDisplay,
|
||||||
|
ProgressDisplay
|
||||||
|
)
|
||||||
|
|
||||||
|
message = f'Downloading to {output_file.name}'
|
||||||
|
if self.env.show_displays:
|
||||||
|
if self.total_size is None:
|
||||||
|
# Rich does not support progress bars without a total
|
||||||
|
# size given. Instead we use status objects.
|
||||||
|
self.display = StatusDisplay(self.env)
|
||||||
|
else:
|
||||||
|
self.display = ProgressDisplay(self.env)
|
||||||
|
else:
|
||||||
|
self.display = DummyDisplay(self.env)
|
||||||
|
|
||||||
|
self.display.start(
|
||||||
|
total=self.total_size,
|
||||||
|
at=self.downloaded,
|
||||||
|
description=message
|
||||||
|
)
|
||||||
|
|
||||||
def chunk_downloaded(self, size):
|
def chunk_downloaded(self, size):
|
||||||
assert self.time_finished is None
|
assert self.time_finished is None
|
||||||
self.downloaded += size
|
self.downloaded += size
|
||||||
|
self.display.update(size)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_finished(self):
|
def has_finished(self):
|
||||||
return self.time_finished is not None
|
return self.time_finished is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_spent(self):
|
||||||
|
if (
|
||||||
|
self.time_started is not None
|
||||||
|
and self.time_finished is not None
|
||||||
|
):
|
||||||
|
return self.time_finished - self.time_started
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def finished(self):
|
def finished(self):
|
||||||
assert self.time_started is not None
|
assert self.time_started is not None
|
||||||
assert self.time_finished is None
|
assert self.time_finished is None
|
||||||
self.time_finished = monotonic()
|
self.time_finished = monotonic()
|
||||||
|
if hasattr(self, 'display'):
|
||||||
|
self.display.stop(self.time_spent)
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
class ProgressReporterThread(threading.Thread):
|
if hasattr(self, 'display'):
|
||||||
"""
|
self.display.stop(self.time_spent)
|
||||||
Reports download progress based on its status.
|
|
||||||
|
|
||||||
Uses threading to periodically update the status (speed, ETA, etc.).
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
status: DownloadStatus,
|
|
||||||
output: IO,
|
|
||||||
tick=.1,
|
|
||||||
update_interval=1
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
self.status = status
|
|
||||||
self.output = output
|
|
||||||
self._tick = tick
|
|
||||||
self._update_interval = update_interval
|
|
||||||
self._spinner_pos = 0
|
|
||||||
self._status_line = ''
|
|
||||||
self._prev_bytes = 0
|
|
||||||
self._prev_time = monotonic()
|
|
||||||
self._should_stop = threading.Event()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Stop reporting on next tick."""
|
|
||||||
self._should_stop.set()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while not self._should_stop.is_set():
|
|
||||||
if self.status.has_finished:
|
|
||||||
self.sum_up()
|
|
||||||
break
|
|
||||||
|
|
||||||
self.report_speed()
|
|
||||||
sleep(self._tick)
|
|
||||||
|
|
||||||
def report_speed(self):
|
|
||||||
now = monotonic()
|
|
||||||
if now - self._prev_time >= self._update_interval:
|
|
||||||
downloaded = self.status.downloaded
|
|
||||||
speed = ((downloaded - self._prev_bytes)
|
|
||||||
/ (now - self._prev_time))
|
|
||||||
|
|
||||||
if not self.status.total_size:
|
|
||||||
self._status_line = PROGRESS_NO_CONTENT_LENGTH.format(
|
|
||||||
downloaded=humanize_bytes(downloaded),
|
|
||||||
speed=humanize_bytes(speed),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
percentage = (downloaded / self.status.total_size * 100
|
|
||||||
if self.status.total_size
|
|
||||||
else 0)
|
|
||||||
|
|
||||||
if not speed:
|
|
||||||
eta = '-:--:--'
|
|
||||||
else:
|
|
||||||
s = int((self.status.total_size - downloaded) / speed)
|
|
||||||
h, s = divmod(s, 60 * 60)
|
|
||||||
m, s = divmod(s, 60)
|
|
||||||
eta = f'{h}:{m:0>2}:{s:0>2}'
|
|
||||||
|
|
||||||
self._status_line = PROGRESS.format(
|
|
||||||
percentage=percentage,
|
|
||||||
downloaded=humanize_bytes(downloaded),
|
|
||||||
speed=humanize_bytes(speed),
|
|
||||||
eta=eta,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._prev_time = now
|
|
||||||
self._prev_bytes = downloaded
|
|
||||||
|
|
||||||
self.output.write(
|
|
||||||
f'{CLEAR_LINE} {SPINNER[self._spinner_pos]} {self._status_line}'
|
|
||||||
)
|
|
||||||
self.output.flush()
|
|
||||||
|
|
||||||
self._spinner_pos = (self._spinner_pos + 1) % len(SPINNER)
|
|
||||||
|
|
||||||
def sum_up(self):
|
|
||||||
actually_downloaded = (
|
|
||||||
self.status.downloaded - self.status.resumed_from)
|
|
||||||
time_taken = self.status.time_finished - self.status.time_started
|
|
||||||
speed = actually_downloaded / time_taken if time_taken else actually_downloaded
|
|
||||||
|
|
||||||
self.output.write(CLEAR_LINE)
|
|
||||||
|
|
||||||
self.output.write(SUMMARY.format(
|
|
||||||
downloaded=humanize_bytes(actually_downloaded),
|
|
||||||
total=(self.status.total_size
|
|
||||||
and humanize_bytes(self.status.total_size)),
|
|
||||||
speed=humanize_bytes(speed),
|
|
||||||
time=time_taken,
|
|
||||||
))
|
|
||||||
self.output.flush()
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from httpie.cli.argparser import HTTPieManagerArgumentParser
|
from httpie.cli.argparser import HTTPieManagerArgumentParser
|
||||||
|
from httpie.cli.options import Qualifiers, ARGPARSE_QUALIFIER_MAP, map_qualifiers, parser_to_parser_spec
|
||||||
from httpie import __version__
|
from httpie import __version__
|
||||||
|
|
||||||
CLI_SESSION_UPGRADE_FLAGS = [
|
CLI_SESSION_UPGRADE_FLAGS = [
|
||||||
@ -58,7 +59,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
|||||||
'or from a local paths.',
|
'or from a local paths.',
|
||||||
{
|
{
|
||||||
'dest': 'targets',
|
'dest': 'targets',
|
||||||
'nargs': '+',
|
'metavar': 'TARGET',
|
||||||
|
'nargs': Qualifiers.ONE_OR_MORE,
|
||||||
'help': 'targets to install'
|
'help': 'targets to install'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -66,7 +68,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
|||||||
'Upgrade the given plugins',
|
'Upgrade the given plugins',
|
||||||
{
|
{
|
||||||
'dest': 'targets',
|
'dest': 'targets',
|
||||||
'nargs': '+',
|
'metavar': 'TARGET',
|
||||||
|
'nargs': Qualifiers.ONE_OR_MORE,
|
||||||
'help': 'targets to upgrade'
|
'help': 'targets to upgrade'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -74,7 +77,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
|||||||
'Uninstall the given HTTPie plugins.',
|
'Uninstall the given HTTPie plugins.',
|
||||||
{
|
{
|
||||||
'dest': 'targets',
|
'dest': 'targets',
|
||||||
'nargs': '+',
|
'metavar': 'TARGET',
|
||||||
|
'nargs': Qualifiers.ONE_OR_MORE,
|
||||||
'help': 'targets to install'
|
'help': 'targets to install'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -94,7 +98,7 @@ def missing_subcommand(*args) -> str:
|
|||||||
return f'Please specify one of these: {subcommands}'
|
return f'Please specify one of these: {subcommands}'
|
||||||
|
|
||||||
|
|
||||||
def generate_subparsers(root, parent_parser, definitions):
|
def generate_subparsers(root, parent_parser, definitions, spec):
|
||||||
action_dest = '_'.join(parent_parser.prog.split()[1:] + ['action'])
|
action_dest = '_'.join(parent_parser.prog.split()[1:] + ['action'])
|
||||||
actions = parent_parser.add_subparsers(
|
actions = parent_parser.add_subparsers(
|
||||||
dest=action_dest
|
dest=action_dest
|
||||||
@ -107,13 +111,15 @@ def generate_subparsers(root, parent_parser, definitions):
|
|||||||
command_parser = actions.add_parser(command, description=descr)
|
command_parser = actions.add_parser(command, description=descr)
|
||||||
command_parser.root = root
|
command_parser.root = root
|
||||||
if is_subparser:
|
if is_subparser:
|
||||||
generate_subparsers(root, command_parser, properties)
|
generate_subparsers(root, command_parser, properties, spec)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
group = spec.add_group(parent_parser.prog + ' ' + command, description=descr)
|
||||||
for argument in properties:
|
for argument in properties:
|
||||||
argument = argument.copy()
|
argument = argument.copy()
|
||||||
flags = argument.pop('flags', [])
|
flags = argument.pop('flags', [])
|
||||||
command_parser.add_argument(*flags, **argument)
|
command_parser.add_argument(*flags, **map_qualifiers(argument, ARGPARSE_QUALIFIER_MAP))
|
||||||
|
group.add_argument(*flags, **argument)
|
||||||
|
|
||||||
|
|
||||||
parser = HTTPieManagerArgumentParser(
|
parser = HTTPieManagerArgumentParser(
|
||||||
@ -160,4 +166,5 @@ parser.add_argument(
|
|||||||
'''
|
'''
|
||||||
)
|
)
|
||||||
|
|
||||||
generate_subparsers(parser, parser, COMMANDS)
|
options = parser_to_parser_spec(parser)
|
||||||
|
generate_subparsers(parser, parser, COMMANDS, options)
|
||||||
|
@ -17,12 +17,11 @@ from pygments.util import ClassNotFound
|
|||||||
|
|
||||||
from ..lexers.json import EnhancedJsonLexer
|
from ..lexers.json import EnhancedJsonLexer
|
||||||
from ..lexers.metadata import MetadataLexer
|
from ..lexers.metadata import MetadataLexer
|
||||||
from ..ui.palette import SHADE_NAMES, get_color
|
from ..ui.palette import AUTO_STYLE, SHADE_NAMES, get_color
|
||||||
from ...context import Environment
|
from ...context import Environment
|
||||||
from ...plugins import FormatterPlugin
|
from ...plugins import FormatterPlugin
|
||||||
|
|
||||||
|
|
||||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
|
||||||
DEFAULT_STYLE = AUTO_STYLE
|
DEFAULT_STYLE = AUTO_STYLE
|
||||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
SOLARIZED_STYLE = 'solarized' # Bundled here
|
||||||
|
|
||||||
@ -33,7 +32,7 @@ BUNDLED_STYLES = {
|
|||||||
|
|
||||||
|
|
||||||
def get_available_styles():
|
def get_available_styles():
|
||||||
return BUNDLED_STYLES | set(pygments.styles.get_all_styles())
|
return sorted(BUNDLED_STYLES | set(pygments.styles.get_all_styles()))
|
||||||
|
|
||||||
|
|
||||||
class ColorFormatter(FormatterPlugin):
|
class ColorFormatter(FormatterPlugin):
|
||||||
|
33
httpie/output/ui/man_pages.py
Normal file
33
httpie/output/ui/man_pages.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""Logic for checking and displaying man pages."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from httpie.context import Environment
|
||||||
|
|
||||||
|
MAN_COMMAND = 'man'
|
||||||
|
NO_MAN_PAGES = os.getenv('HTTPIE_NO_MAN_PAGES', False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_available(program: str) -> bool:
|
||||||
|
"""Check whether HTTPie's man pages are available in this system."""
|
||||||
|
|
||||||
|
if NO_MAN_PAGES or os.system == 'nt':
|
||||||
|
return False
|
||||||
|
|
||||||
|
process = subprocess.run(
|
||||||
|
[MAN_COMMAND, program],
|
||||||
|
shell=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
return process.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def display_for(env: Environment, program: str) -> None:
|
||||||
|
"""Display the man page for the given command (http/https)."""
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[MAN_COMMAND, program],
|
||||||
|
stdout=env.stdout,
|
||||||
|
stderr=env.stderr
|
||||||
|
)
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||||
STYLE_PIE = 'pie'
|
STYLE_PIE = 'pie'
|
||||||
STYLE_PIE_DARK = 'pie-dark'
|
STYLE_PIE_DARK = 'pie-dark'
|
||||||
STYLE_PIE_LIGHT = 'pie-light'
|
STYLE_PIE_LIGHT = 'pie-light'
|
||||||
@ -7,8 +8,6 @@ STYLE_PIE_LIGHT = 'pie-light'
|
|||||||
|
|
||||||
COLOR_PALETTE = {
|
COLOR_PALETTE = {
|
||||||
# Copy the brand palette
|
# Copy the brand palette
|
||||||
'transparent': 'transparent',
|
|
||||||
'current': 'currentColor',
|
|
||||||
'white': '#F5F5F0',
|
'white': '#F5F5F0',
|
||||||
'black': '#1C1818',
|
'black': '#1C1818',
|
||||||
'grey': {
|
'grey': {
|
||||||
@ -150,17 +149,27 @@ SHADE_NAMES = {
|
|||||||
'700': STYLE_PIE_LIGHT
|
'700': STYLE_PIE_LIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STYLE_SHADES = {
|
||||||
|
style: shade
|
||||||
|
for shade, style in SHADE_NAMES.items()
|
||||||
|
}
|
||||||
|
|
||||||
SHADES = [
|
SHADES = [
|
||||||
'50',
|
'50',
|
||||||
*map(str, range(100, 1000, 100))
|
*map(str, range(100, 1000, 100))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_color(color: str, shade: str) -> Optional[str]:
|
def get_color(
|
||||||
if color not in COLOR_PALETTE:
|
color: str,
|
||||||
|
shade: str,
|
||||||
|
*,
|
||||||
|
palette: Dict[str, Dict[str, str]] = COLOR_PALETTE
|
||||||
|
) -> Optional[str]:
|
||||||
|
if color not in palette:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
color_code = COLOR_PALETTE[color]
|
color_code = palette[color]
|
||||||
if isinstance(color_code, dict) and shade in color_code:
|
if isinstance(color_code, dict) and shade in color_code:
|
||||||
return color_code[shade]
|
return color_code[shade]
|
||||||
else:
|
else:
|
||||||
|
217
httpie/output/ui/rich_help.py
Normal file
217
httpie/output/ui/rich_help.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
from typing import AbstractSet, Iterable, Optional, Tuple
|
||||||
|
|
||||||
|
from rich.console import RenderableType
|
||||||
|
from rich.highlighter import RegexHighlighter
|
||||||
|
from rich.padding import Padding
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from httpie.cli.constants import SEPARATOR_GROUP_ALL_ITEMS
|
||||||
|
from httpie.cli.options import Argument, ParserSpec, Qualifiers
|
||||||
|
|
||||||
|
SEPARATORS = '|'.join(map(re.escape, SEPARATOR_GROUP_ALL_ITEMS))
|
||||||
|
|
||||||
|
STYLE_METAVAR = 'yellow'
|
||||||
|
STYLE_SWITCH = 'green'
|
||||||
|
STYLE_PROGRAM_NAME = 'bold green'
|
||||||
|
STYLE_USAGE_OPTIONAL = 'grey46'
|
||||||
|
STYLE_USAGE_REGULAR = 'white'
|
||||||
|
STYLE_USAGE_ERROR = 'red'
|
||||||
|
STYLE_USAGE_MISSING = 'yellow'
|
||||||
|
|
||||||
|
MAX_CHOICE_CHARS = 80
|
||||||
|
|
||||||
|
LEFT_PADDING_2 = (0, 0, 0, 2)
|
||||||
|
LEFT_PADDING_4 = (0, 0, 0, 4)
|
||||||
|
LEFT_PADDING_5 = (0, 0, 0, 4)
|
||||||
|
|
||||||
|
LEFT_INDENT_2 = (1, 0, 0, 2)
|
||||||
|
LEFT_INDENT_3 = (1, 0, 0, 3)
|
||||||
|
LEFT_INDENT_BOTTOM_3 = (0, 0, 1, 3)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsHighlighter(RegexHighlighter):
|
||||||
|
highlights = [
|
||||||
|
r'(^|\W)(?P<option>\-{1,2}[\w|-]+)(?![a-zA-Z0-9])',
|
||||||
|
r'(?P<bold>HTTPie)',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
options_highlighter = OptionsHighlighter()
|
||||||
|
|
||||||
|
|
||||||
|
def unpack_argument(
|
||||||
|
argument: Argument,
|
||||||
|
) -> Tuple[Text, Text]:
|
||||||
|
opt1 = opt2 = ''
|
||||||
|
|
||||||
|
style = None
|
||||||
|
if argument.aliases:
|
||||||
|
if len(argument.aliases) >= 2:
|
||||||
|
opt2, opt1 = argument.aliases
|
||||||
|
else:
|
||||||
|
(opt1,) = argument.aliases
|
||||||
|
else:
|
||||||
|
opt1 = argument.metavar
|
||||||
|
style = STYLE_USAGE_REGULAR
|
||||||
|
|
||||||
|
return Text(opt1, style=style), Text(opt2)
|
||||||
|
|
||||||
|
|
||||||
|
def to_usage(
|
||||||
|
spec: ParserSpec,
|
||||||
|
*,
|
||||||
|
program_name: Optional[str] = None,
|
||||||
|
whitelist: AbstractSet[str] = frozenset()
|
||||||
|
) -> RenderableType:
|
||||||
|
shown_arguments = [
|
||||||
|
argument
|
||||||
|
for group in spec.groups
|
||||||
|
for argument in group.arguments
|
||||||
|
if (not argument.aliases or whitelist.intersection(argument.aliases))
|
||||||
|
]
|
||||||
|
|
||||||
|
# Sort the shown_arguments so that --dash options are
|
||||||
|
# shown first
|
||||||
|
shown_arguments.sort(key=lambda argument: argument.aliases, reverse=True)
|
||||||
|
|
||||||
|
text = Text(program_name or spec.program, style='bold')
|
||||||
|
for argument in shown_arguments:
|
||||||
|
text.append(' ')
|
||||||
|
|
||||||
|
is_whitelisted = whitelist.intersection(argument.aliases)
|
||||||
|
if argument.aliases:
|
||||||
|
name = '/'.join(sorted(argument.aliases, key=len))
|
||||||
|
else:
|
||||||
|
name = argument.metavar
|
||||||
|
|
||||||
|
nargs = argument.configuration.get('nargs')
|
||||||
|
if nargs is Qualifiers.OPTIONAL:
|
||||||
|
text.append('[' + name + ']', style=STYLE_USAGE_OPTIONAL)
|
||||||
|
elif nargs is Qualifiers.ZERO_OR_MORE:
|
||||||
|
text.append(
|
||||||
|
'[' + name + ' ...]',
|
||||||
|
style=STYLE_USAGE_OPTIONAL,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text.append(
|
||||||
|
name,
|
||||||
|
style=STYLE_USAGE_ERROR
|
||||||
|
if is_whitelisted
|
||||||
|
else STYLE_USAGE_REGULAR,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_form = argument.serialize()
|
||||||
|
if raw_form.get('choices'):
|
||||||
|
text.append(' ')
|
||||||
|
text.append(
|
||||||
|
'{' + ', '.join(raw_form['choices']) + '}',
|
||||||
|
style=STYLE_USAGE_MISSING,
|
||||||
|
)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# This part is loosely based on the rich-click's help message
|
||||||
|
# generation.
|
||||||
|
def to_help_message(
|
||||||
|
spec: ParserSpec,
|
||||||
|
) -> Iterable[RenderableType]:
|
||||||
|
yield Padding(
|
||||||
|
options_highlighter(spec.description),
|
||||||
|
LEFT_INDENT_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield Padding(
|
||||||
|
Text('Usage', style=STYLE_SWITCH),
|
||||||
|
LEFT_INDENT_2,
|
||||||
|
)
|
||||||
|
yield Padding(to_usage(spec), LEFT_INDENT_3)
|
||||||
|
|
||||||
|
group_rows = {}
|
||||||
|
for group in spec.groups:
|
||||||
|
options_rows = []
|
||||||
|
|
||||||
|
for argument in group.arguments:
|
||||||
|
if argument.is_hidden:
|
||||||
|
continue
|
||||||
|
|
||||||
|
opt1, opt2 = unpack_argument(argument)
|
||||||
|
if opt2:
|
||||||
|
opt1.append('/')
|
||||||
|
opt1.append(opt2)
|
||||||
|
|
||||||
|
# Column for a metavar, if we have one
|
||||||
|
metavar = Text(style=STYLE_METAVAR)
|
||||||
|
metavar.append(argument.configuration.get('metavar', ''))
|
||||||
|
|
||||||
|
if opt1 == metavar:
|
||||||
|
metavar = Text('')
|
||||||
|
|
||||||
|
raw_form = argument.serialize()
|
||||||
|
desc = raw_form.get('short_description', '')
|
||||||
|
if raw_form.get('choices'):
|
||||||
|
desc += ' (choices: '
|
||||||
|
desc += textwrap.shorten(
|
||||||
|
', '.join(raw_form.get('choices')),
|
||||||
|
MAX_CHOICE_CHARS,
|
||||||
|
)
|
||||||
|
desc += ')'
|
||||||
|
|
||||||
|
rows = [
|
||||||
|
Padding(
|
||||||
|
options_highlighter(opt1),
|
||||||
|
LEFT_PADDING_2,
|
||||||
|
),
|
||||||
|
metavar,
|
||||||
|
options_highlighter(desc),
|
||||||
|
]
|
||||||
|
|
||||||
|
options_rows.append(rows)
|
||||||
|
if argument.configuration.get('nested_options'):
|
||||||
|
options_rows.extend(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
Padding(
|
||||||
|
Text(
|
||||||
|
key,
|
||||||
|
style=STYLE_USAGE_OPTIONAL,
|
||||||
|
),
|
||||||
|
LEFT_PADDING_4,
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
dec,
|
||||||
|
)
|
||||||
|
for key, value, dec in argument.nested_options
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
group_rows[group.name] = options_rows
|
||||||
|
|
||||||
|
options_table = Table(highlight=False, box=None, show_header=False)
|
||||||
|
for group_name, options_rows in group_rows.items():
|
||||||
|
options_table.add_row(Text(), Text(), Text())
|
||||||
|
options_table.add_row(
|
||||||
|
Text(group_name, style=STYLE_SWITCH),
|
||||||
|
Text(),
|
||||||
|
Text(),
|
||||||
|
)
|
||||||
|
options_table.add_row(Text(), Text(), Text())
|
||||||
|
for row in options_rows:
|
||||||
|
options_table.add_row(*row)
|
||||||
|
|
||||||
|
yield Padding(
|
||||||
|
Text('Options', style=STYLE_SWITCH),
|
||||||
|
LEFT_INDENT_2,
|
||||||
|
)
|
||||||
|
yield Padding(options_table, LEFT_PADDING_2)
|
||||||
|
yield Padding(
|
||||||
|
Text('More Information', style=STYLE_SWITCH),
|
||||||
|
LEFT_INDENT_2,
|
||||||
|
)
|
||||||
|
yield Padding(
|
||||||
|
spec.epilog.rstrip('\n'),
|
||||||
|
LEFT_INDENT_BOTTOM_3,
|
||||||
|
)
|
23
httpie/output/ui/rich_palette.py
Normal file
23
httpie/output/ui/rich_palette.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from httpie.output.ui.palette import * # noqa
|
||||||
|
|
||||||
|
# Rich-specific color code declarations
|
||||||
|
# https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py#L5
|
||||||
|
CUSTOM_STYLES = {
|
||||||
|
'progress.description': 'white',
|
||||||
|
'progress.data.speed': 'green',
|
||||||
|
'progress.percentage': 'aqua',
|
||||||
|
'progress.download': 'aqua',
|
||||||
|
'progress.remaining': 'orange',
|
||||||
|
'bar.complete': 'purple',
|
||||||
|
'bar.finished': 'green',
|
||||||
|
'bar.pulse': 'purple',
|
||||||
|
'option': 'pink'
|
||||||
|
}
|
||||||
|
|
||||||
|
RICH_THEME_PALETTE = COLOR_PALETTE.copy() # noqa
|
||||||
|
RICH_THEME_PALETTE.update(
|
||||||
|
{
|
||||||
|
custom_style: RICH_THEME_PALETTE[color]
|
||||||
|
for custom_style, color in CUSTOM_STYLES.items()
|
||||||
|
}
|
||||||
|
)
|
136
httpie/output/ui/rich_progress.py
Normal file
136
httpie/output/ui/rich_progress.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from httpie.context import Environment
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseDisplay:
|
||||||
|
env: Environment
|
||||||
|
|
||||||
|
def start(
|
||||||
|
self, *, total: Optional[float], at: float, description: str
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def update(self, steps: float) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def stop(self, time_spent: float) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def console(self) -> 'Console':
|
||||||
|
"""Returns the default console to be used with displays (stderr)."""
|
||||||
|
return self.env.rich_error_console
|
||||||
|
|
||||||
|
def _print_summary(
|
||||||
|
self,
|
||||||
|
is_finished: bool,
|
||||||
|
observed_steps: int,
|
||||||
|
time_spent: float
|
||||||
|
):
|
||||||
|
from rich import filesize
|
||||||
|
|
||||||
|
if is_finished:
|
||||||
|
verb = 'Done'
|
||||||
|
else:
|
||||||
|
verb = 'Interrupted'
|
||||||
|
|
||||||
|
total_size = filesize.decimal(observed_steps)
|
||||||
|
avg_speed = filesize.decimal(observed_steps / time_spent)
|
||||||
|
|
||||||
|
minutes, seconds = divmod(time_spent, 60)
|
||||||
|
hours, minutes = divmod(int(minutes), 60)
|
||||||
|
if hours:
|
||||||
|
total_time = f'{hours:d}:{minutes:02d}:{seconds:0.5f}'
|
||||||
|
else:
|
||||||
|
total_time = f'{minutes:02d}:{seconds:0.5f}'
|
||||||
|
|
||||||
|
self.console.print(f'[progress.description]{verb}. {total_size} in {total_time} ({avg_speed}/s)')
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDisplay(BaseDisplay):
|
||||||
|
"""
|
||||||
|
A dummy display object to be used when the progress bars,
|
||||||
|
spinners etc. are disabled globally (or during tests).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class StatusDisplay(BaseDisplay):
|
||||||
|
def start(
|
||||||
|
self, *, total: Optional[float], at: float, description: str
|
||||||
|
) -> None:
|
||||||
|
self.observed = at
|
||||||
|
self.description = f'[progress.description]{description}[/progress.description]'
|
||||||
|
|
||||||
|
self.status = self.console.status(self.description, spinner='line')
|
||||||
|
self.status.start()
|
||||||
|
|
||||||
|
def update(self, steps: float) -> None:
|
||||||
|
from rich import filesize
|
||||||
|
|
||||||
|
self.observed += steps
|
||||||
|
|
||||||
|
observed_amount, observed_unit = filesize.decimal(self.observed).split()
|
||||||
|
self.status.update(status=f'{self.description} [progress.download]{observed_amount}/? {observed_unit}[/progress.download]')
|
||||||
|
|
||||||
|
def stop(self, time_spent: float) -> None:
|
||||||
|
self.status.stop()
|
||||||
|
self.console.print(self.description)
|
||||||
|
if time_spent:
|
||||||
|
self._print_summary(
|
||||||
|
is_finished=True,
|
||||||
|
observed_steps=self.observed,
|
||||||
|
time_spent=time_spent
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDisplay(BaseDisplay):
|
||||||
|
def start(
|
||||||
|
self, *, total: Optional[float], at: float, description: str
|
||||||
|
) -> None:
|
||||||
|
from rich.progress import (
|
||||||
|
Progress,
|
||||||
|
BarColumn,
|
||||||
|
DownloadColumn,
|
||||||
|
TimeRemainingColumn,
|
||||||
|
TransferSpeedColumn,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert total is not None
|
||||||
|
self.console.print(f'[progress.description]{description}')
|
||||||
|
self.progress_bar = Progress(
|
||||||
|
'[',
|
||||||
|
BarColumn(),
|
||||||
|
']',
|
||||||
|
'[progress.percentage]{task.percentage:>3.0f}%',
|
||||||
|
'(',
|
||||||
|
DownloadColumn(),
|
||||||
|
')',
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
console=self.console,
|
||||||
|
transient=True
|
||||||
|
)
|
||||||
|
self.progress_bar.start()
|
||||||
|
self.transfer_task = self.progress_bar.add_task(
|
||||||
|
description, completed=at, total=total
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, steps: float) -> None:
|
||||||
|
self.progress_bar.advance(self.transfer_task, steps)
|
||||||
|
|
||||||
|
def stop(self, time_spent: Optional[float]) -> None:
|
||||||
|
self.progress_bar.stop()
|
||||||
|
|
||||||
|
if time_spent:
|
||||||
|
[task] = self.progress_bar.tasks
|
||||||
|
self._print_summary(
|
||||||
|
is_finished=task.finished,
|
||||||
|
observed_steps=task.completed,
|
||||||
|
time_spent=time_spent
|
||||||
|
)
|
35
httpie/output/ui/rich_utils.py
Normal file
35
httpie/output/ui/rich_utils.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from typing import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from rich.console import Console, RenderableType
|
||||||
|
from rich.highlighter import Highlighter
|
||||||
|
|
||||||
|
|
||||||
|
def render_as_string(renderable: RenderableType) -> str:
|
||||||
|
"""Render any `rich` object in a fake console and
|
||||||
|
return a *style-less* version of it as a string."""
|
||||||
|
|
||||||
|
with open(os.devnull, "w") as null_stream:
|
||||||
|
fake_console = Console(
|
||||||
|
file=null_stream,
|
||||||
|
record=True
|
||||||
|
)
|
||||||
|
fake_console.print(renderable)
|
||||||
|
return fake_console.export_text()
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def enable_highlighter(
|
||||||
|
console: Console,
|
||||||
|
highlighter: Highlighter,
|
||||||
|
) -> Iterator[Console]:
|
||||||
|
"""Enable a higlighter temporarily."""
|
||||||
|
|
||||||
|
original_highlighter = console.highlighter
|
||||||
|
try:
|
||||||
|
console.highlighter = highlighter
|
||||||
|
yield console
|
||||||
|
finally:
|
||||||
|
console.highlighter = original_highlighter
|
5
setup.py
5
setup.py
@ -38,6 +38,7 @@ install_requires = [
|
|||||||
'multidict>=4.7.0',
|
'multidict>=4.7.0',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'importlib-metadata>=1.4.0; python_version < "3.8"',
|
'importlib-metadata>=1.4.0; python_version < "3.8"',
|
||||||
|
'rich>=9.10.0'
|
||||||
]
|
]
|
||||||
install_requires_win_only = [
|
install_requires_win_only = [
|
||||||
'colorama>=0.2.4',
|
'colorama>=0.2.4',
|
||||||
@ -111,4 +112,8 @@ setup(
|
|||||||
'Documentation': 'https://httpie.io/docs',
|
'Documentation': 'https://httpie.io/docs',
|
||||||
'Online Demo': 'https://httpie.io/run',
|
'Online Demo': 'https://httpie.io/run',
|
||||||
},
|
},
|
||||||
|
data_files=[
|
||||||
|
('share/man/man1', ['extras/man/http.1']),
|
||||||
|
('share/man/man1', ['extras/man/https.1']),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,54 +1,37 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from tests.utils import http
|
from tests.utils import http
|
||||||
|
|
||||||
|
NAKED_BASE_TEMPLATE = """\
|
||||||
if sys.version_info >= (3, 9):
|
|
||||||
REQUEST_ITEM_MSG = "[REQUEST_ITEM ...]"
|
|
||||||
else:
|
|
||||||
REQUEST_ITEM_MSG = "[REQUEST_ITEM [REQUEST_ITEM ...]]"
|
|
||||||
|
|
||||||
|
|
||||||
NAKED_HELP_MESSAGE = f"""\
|
|
||||||
usage:
|
usage:
|
||||||
http [METHOD] URL {REQUEST_ITEM_MSG}
|
http {extra_args}[METHOD] URL [REQUEST_ITEM ...]
|
||||||
|
|
||||||
error:
|
error:
|
||||||
the following arguments are required: URL
|
{error_msg}
|
||||||
|
|
||||||
for more information:
|
for more information:
|
||||||
run 'http --help' or visit https://httpie.io/docs/cli
|
run 'http --help' or visit https://httpie.io/docs/cli
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG = f"""\
|
NAKED_HELP_MESSAGE = NAKED_BASE_TEMPLATE.format(
|
||||||
usage:
|
extra_args="",
|
||||||
http [--pretty {{all,colors,format,none}}] [METHOD] URL {REQUEST_ITEM_MSG}
|
error_msg="the following arguments are required: URL"
|
||||||
|
)
|
||||||
|
|
||||||
error:
|
NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG = NAKED_BASE_TEMPLATE.format(
|
||||||
argument --pretty: expected one argument
|
extra_args="--pretty {all, colors, format, none} ",
|
||||||
|
error_msg="argument --pretty: expected one argument"
|
||||||
|
)
|
||||||
|
|
||||||
for more information:
|
NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG = NAKED_BASE_TEMPLATE.format(
|
||||||
run 'http --help' or visit https://httpie.io/docs/cli
|
extra_args="--pretty {all, colors, format, none} ",
|
||||||
|
error_msg="argument --pretty: invalid choice: '$invalid' (choose from 'all', 'colors', 'format', 'none')"
|
||||||
"""
|
)
|
||||||
|
|
||||||
NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG = f"""\
|
|
||||||
usage:
|
|
||||||
http [--pretty {{all,colors,format,none}}] [METHOD] URL {REQUEST_ITEM_MSG}
|
|
||||||
|
|
||||||
error:
|
|
||||||
argument --pretty: invalid choice: '$invalid' (choose from 'all', 'colors', 'format', 'none')
|
|
||||||
|
|
||||||
for more information:
|
|
||||||
run 'http --help' or visit https://httpie.io/docs/cli
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
PREDEFINED_TERMINAL_SIZE = (160, 80)
|
PREDEFINED_TERMINAL_SIZE = (200, 100)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -66,6 +49,7 @@ def ignore_terminal_size(monkeypatch):
|
|||||||
# Setting COLUMNS as an env var is required for 3.8<
|
# Setting COLUMNS as an env var is required for 3.8<
|
||||||
monkeypatch.setitem(os.environ, 'COLUMNS', str(PREDEFINED_TERMINAL_SIZE[0]))
|
monkeypatch.setitem(os.environ, 'COLUMNS', str(PREDEFINED_TERMINAL_SIZE[0]))
|
||||||
monkeypatch.setattr(shutil, 'get_terminal_size', fake_terminal_size)
|
monkeypatch.setattr(shutil, 'get_terminal_size', fake_terminal_size)
|
||||||
|
monkeypatch.setattr(os, 'get_terminal_size', fake_terminal_size)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -83,4 +83,4 @@ def test_lazy_choices_help():
|
|||||||
# If we use --help, then we call it with styles
|
# If we use --help, then we call it with styles
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
parser.parse_args(['--help'])
|
parser.parse_args(['--help'])
|
||||||
help_formatter.assert_called_once_with(['a', 'b', 'c'])
|
help_formatter.assert_called_once_with(['a', 'b', 'c'], isolation_mode=False)
|
||||||
|
@ -125,16 +125,14 @@ class TestDownloads:
|
|||||||
def test_actual_download(self, httpbin_both, httpbin):
|
def test_actual_download(self, httpbin_both, httpbin):
|
||||||
robots_txt = '/robots.txt'
|
robots_txt = '/robots.txt'
|
||||||
body = urlopen(httpbin + robots_txt).read().decode()
|
body = urlopen(httpbin + robots_txt).read().decode()
|
||||||
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False)
|
env = MockEnvironment(stdin_isatty=True, stdout_isatty=False, show_displays=True)
|
||||||
r = http('--download', httpbin_both.url + robots_txt, env=env)
|
r = http('--download', httpbin_both.url + robots_txt, env=env)
|
||||||
assert 'Downloading' in r.stderr
|
assert 'Downloading' in r.stderr
|
||||||
assert '[K' in r.stderr
|
|
||||||
assert 'Done' in r.stderr
|
|
||||||
assert body == r
|
assert body == r
|
||||||
|
|
||||||
def test_download_with_Content_Length(self, httpbin_both):
|
def test_download_with_Content_Length(self, mock_env, httpbin_both):
|
||||||
with open(os.devnull, 'w') as devnull:
|
with open(os.devnull, 'w') as devnull:
|
||||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
downloader = Downloader(mock_env, output_file=devnull)
|
||||||
downloader.start(
|
downloader.start(
|
||||||
initial_url='/',
|
initial_url='/',
|
||||||
final_response=Response(
|
final_response=Response(
|
||||||
@ -148,11 +146,10 @@ class TestDownloads:
|
|||||||
downloader.chunk_downloaded(b'12345')
|
downloader.chunk_downloaded(b'12345')
|
||||||
downloader.finish()
|
downloader.finish()
|
||||||
assert not downloader.interrupted
|
assert not downloader.interrupted
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
def test_download_no_Content_Length(self, httpbin_both):
|
def test_download_no_Content_Length(self, mock_env, httpbin_both):
|
||||||
with open(os.devnull, 'w') as devnull:
|
with open(os.devnull, 'w') as devnull:
|
||||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
downloader = Downloader(mock_env, output_file=devnull)
|
||||||
downloader.start(
|
downloader.start(
|
||||||
final_response=Response(url=httpbin_both.url + '/'),
|
final_response=Response(url=httpbin_both.url + '/'),
|
||||||
initial_url='/'
|
initial_url='/'
|
||||||
@ -161,15 +158,14 @@ class TestDownloads:
|
|||||||
downloader.chunk_downloaded(b'12345')
|
downloader.chunk_downloaded(b'12345')
|
||||||
downloader.finish()
|
downloader.finish()
|
||||||
assert not downloader.interrupted
|
assert not downloader.interrupted
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
def test_download_output_from_content_disposition(self, httpbin_both):
|
def test_download_output_from_content_disposition(self, mock_env, httpbin_both):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dirname, open(os.devnull, 'w') as devnull:
|
with tempfile.TemporaryDirectory() as tmp_dirname:
|
||||||
orig_cwd = os.getcwd()
|
orig_cwd = os.getcwd()
|
||||||
os.chdir(tmp_dirname)
|
os.chdir(tmp_dirname)
|
||||||
try:
|
try:
|
||||||
assert not os.path.isfile('filename.bin')
|
assert not os.path.isfile('filename.bin')
|
||||||
downloader = Downloader(progress_file=devnull)
|
downloader = Downloader(mock_env)
|
||||||
downloader.start(
|
downloader.start(
|
||||||
final_response=Response(
|
final_response=Response(
|
||||||
url=httpbin_both.url + '/',
|
url=httpbin_both.url + '/',
|
||||||
@ -184,7 +180,6 @@ class TestDownloads:
|
|||||||
downloader.finish()
|
downloader.finish()
|
||||||
downloader.failed() # Stop the reporter
|
downloader.failed() # Stop the reporter
|
||||||
assert not downloader.interrupted
|
assert not downloader.interrupted
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
# TODO: Auto-close the file in that case?
|
# TODO: Auto-close the file in that case?
|
||||||
downloader._output_file.close()
|
downloader._output_file.close()
|
||||||
@ -192,9 +187,9 @@ class TestDownloads:
|
|||||||
finally:
|
finally:
|
||||||
os.chdir(orig_cwd)
|
os.chdir(orig_cwd)
|
||||||
|
|
||||||
def test_download_interrupted(self, httpbin_both):
|
def test_download_interrupted(self, mock_env, httpbin_both):
|
||||||
with open(os.devnull, 'w') as devnull:
|
with open(os.devnull, 'w') as devnull:
|
||||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
downloader = Downloader(mock_env, output_file=devnull)
|
||||||
downloader.start(
|
downloader.start(
|
||||||
final_response=Response(
|
final_response=Response(
|
||||||
url=httpbin_both.url + '/',
|
url=httpbin_both.url + '/',
|
||||||
@ -205,17 +200,16 @@ class TestDownloads:
|
|||||||
downloader.chunk_downloaded(b'1234')
|
downloader.chunk_downloaded(b'1234')
|
||||||
downloader.finish()
|
downloader.finish()
|
||||||
assert downloader.interrupted
|
assert downloader.interrupted
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
def test_download_resumed(self, httpbin_both):
|
def test_download_resumed(self, mock_env, httpbin_both):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dirname:
|
with tempfile.TemporaryDirectory() as tmp_dirname:
|
||||||
file = os.path.join(tmp_dirname, 'file.bin')
|
file = os.path.join(tmp_dirname, 'file.bin')
|
||||||
with open(file, 'a'):
|
with open(file, 'a'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with open(os.devnull, 'w') as devnull, open(file, 'a+b') as output_file:
|
with open(file, 'a+b') as output_file:
|
||||||
# Start and interrupt the transfer after 3 bytes written
|
# Start and interrupt the transfer after 3 bytes written
|
||||||
downloader = Downloader(output_file=output_file, progress_file=devnull)
|
downloader = Downloader(mock_env, output_file=output_file)
|
||||||
downloader.start(
|
downloader.start(
|
||||||
final_response=Response(
|
final_response=Response(
|
||||||
url=httpbin_both.url + '/',
|
url=httpbin_both.url + '/',
|
||||||
@ -227,15 +221,14 @@ class TestDownloads:
|
|||||||
downloader.finish()
|
downloader.finish()
|
||||||
downloader.failed()
|
downloader.failed()
|
||||||
assert downloader.interrupted
|
assert downloader.interrupted
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
# Write bytes
|
# Write bytes
|
||||||
with open(file, 'wb') as fh:
|
with open(file, 'wb') as fh:
|
||||||
fh.write(b'123')
|
fh.write(b'123')
|
||||||
|
|
||||||
with open(os.devnull, 'w') as devnull, open(file, 'a+b') as output_file:
|
with open(file, 'a+b') as output_file:
|
||||||
# Resume the transfer
|
# Resume the transfer
|
||||||
downloader = Downloader(output_file=output_file, progress_file=devnull, resume=True)
|
downloader = Downloader(mock_env, output_file=output_file, resume=True)
|
||||||
|
|
||||||
# Ensure `pre_request()` is working as expected too
|
# Ensure `pre_request()` is working as expected too
|
||||||
headers = {}
|
headers = {}
|
||||||
@ -253,7 +246,6 @@ class TestDownloads:
|
|||||||
)
|
)
|
||||||
downloader.chunk_downloaded(b'45')
|
downloader.chunk_downloaded(b'45')
|
||||||
downloader.finish()
|
downloader.finish()
|
||||||
downloader._progress_reporter.join()
|
|
||||||
|
|
||||||
def test_download_with_redirect_original_url_used_for_filename(self, httpbin):
|
def test_download_with_redirect_original_url_used_for_filename(self, httpbin):
|
||||||
# Redirect from `/redirect/1` to `/get`.
|
# Redirect from `/redirect/1` to `/get`.
|
||||||
|
@ -97,6 +97,8 @@ class TestQuietFlag:
|
|||||||
(['-q'], 1),
|
(['-q'], 1),
|
||||||
(['-qq'], 0),
|
(['-qq'], 0),
|
||||||
])
|
])
|
||||||
|
# Might fail on Windows due to interference from other warnings.
|
||||||
|
@pytest.mark.xfail
|
||||||
def test_quiet_on_python_warnings(self, test_patch, httpbin, flags, expected_warnings):
|
def test_quiet_on_python_warnings(self, test_patch, httpbin, flags, expected_warnings):
|
||||||
def warn_and_run(*args, **kwargs):
|
def warn_and_run(*args, **kwargs):
|
||||||
warnings.warn('warning!!')
|
warnings.warn('warning!!')
|
||||||
|
@ -5,19 +5,20 @@ def test_parser_serialization():
|
|||||||
small_parser = ParserSpec("test_parser")
|
small_parser = ParserSpec("test_parser")
|
||||||
|
|
||||||
group_1 = small_parser.add_group("group_1")
|
group_1 = small_parser.add_group("group_1")
|
||||||
group_1.add_argument("regular_arg", help="regular arg")
|
group_1.add_argument("regular_arg", help="regular arg", short_help="short")
|
||||||
group_1.add_argument(
|
group_1.add_argument(
|
||||||
"variadic_arg",
|
"variadic_arg",
|
||||||
metavar="META",
|
metavar="META",
|
||||||
help=Qualifiers.SUPPRESS,
|
help=Qualifiers.SUPPRESS,
|
||||||
nargs=Qualifiers.ZERO_OR_MORE,
|
nargs=Qualifiers.ZERO_OR_MORE
|
||||||
)
|
)
|
||||||
group_1.add_argument(
|
group_1.add_argument(
|
||||||
"-O",
|
"-O",
|
||||||
"--opt-arg",
|
"--opt-arg",
|
||||||
action="lazy_choices",
|
action="lazy_choices",
|
||||||
getter=lambda: ["opt_1", "opt_2"],
|
getter=lambda: ["opt_1", "opt_2"],
|
||||||
help_formatter=lambda state: ", ".join(state),
|
help_formatter=lambda state, *, isolation_mode: ", ".join(state),
|
||||||
|
short_help="short_help",
|
||||||
)
|
)
|
||||||
|
|
||||||
group_2 = small_parser.add_group("group_2")
|
group_2 = small_parser.add_group("group_2")
|
||||||
@ -36,6 +37,7 @@ def test_parser_serialization():
|
|||||||
{
|
{
|
||||||
"options": ["regular_arg"],
|
"options": ["regular_arg"],
|
||||||
"description": "regular arg",
|
"description": "regular arg",
|
||||||
|
"short_description": "short",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"options": ["variadic_arg"],
|
"options": ["variadic_arg"],
|
||||||
@ -46,6 +48,7 @@ def test_parser_serialization():
|
|||||||
{
|
{
|
||||||
"options": ["-O", "--opt-arg"],
|
"options": ["-O", "--opt-arg"],
|
||||||
"description": "opt_1, opt_2",
|
"description": "opt_1, opt_2",
|
||||||
|
"short_description": "short_help",
|
||||||
"choices": ["opt_1", "opt_2"],
|
"choices": ["opt_1", "opt_2"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -136,7 +136,7 @@ def test_auto_streaming(http_server, extras, expected):
|
|||||||
assert len([
|
assert len([
|
||||||
call_arg
|
call_arg
|
||||||
for call_arg in env.stdout.write.call_args_list
|
for call_arg in env.stdout.write.call_args_list
|
||||||
if b'test' in call_arg[0][0]
|
if 'test' in call_arg[0][0]
|
||||||
]) == expected
|
]) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import httpie.manager.__main__ as manager
|
|||||||
|
|
||||||
from httpie.status import ExitStatus
|
from httpie.status import ExitStatus
|
||||||
from httpie.config import Config
|
from httpie.config import Config
|
||||||
|
from httpie.encoding import UTF8
|
||||||
from httpie.context import Environment
|
from httpie.context import Environment
|
||||||
from httpie.utils import url_as_host
|
from httpie.utils import url_as_host
|
||||||
|
|
||||||
@ -61,6 +62,59 @@ def add_auth(url, auth):
|
|||||||
return f'{proto}://{auth}@{rest}'
|
return f'{proto}://{auth}@{rest}'
|
||||||
|
|
||||||
|
|
||||||
|
class Encoder:
|
||||||
|
"""
|
||||||
|
Encode binary fragments into a text stream. This is used
|
||||||
|
to embed raw binary data (which can't be decoded) into the
|
||||||
|
fake standard output we use on MockEnvironment.
|
||||||
|
|
||||||
|
Each data fragment is embedded by it's hash:
|
||||||
|
"Some data hash(XXX) more data."
|
||||||
|
|
||||||
|
Which then later converted back to a bytes object:
|
||||||
|
b"Some data <real data> more data."
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEMPLATE = 'hash({})'
|
||||||
|
|
||||||
|
STR_PATTERN = re.compile(r'hash\((.*)\)')
|
||||||
|
BYTES_PATTERN = re.compile(rb'hash\((.*)\)')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.substitutions = {}
|
||||||
|
|
||||||
|
def subsitute(self, data: bytes) -> str:
|
||||||
|
idx = hash(data)
|
||||||
|
self.substitutions[idx] = data
|
||||||
|
return self.TEMPLATE.format(idx)
|
||||||
|
|
||||||
|
def decode(self, data: str) -> Union[str, bytes]:
|
||||||
|
if self.STR_PATTERN.search(data) is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
raw_data = data.encode()
|
||||||
|
return self.BYTES_PATTERN.sub(
|
||||||
|
lambda match: self.substitutions[int(match.group(1))],
|
||||||
|
raw_data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeBytesIOBuffer(BytesIO):
|
||||||
|
|
||||||
|
def __init__(self, original, encoder, *args, **kwargs):
|
||||||
|
self.original_buffer = original
|
||||||
|
self.encoder = encoder
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
try:
|
||||||
|
self.original_buffer.write(data.decode(UTF8))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
self.original_buffer.write(self.encoder.subsitute(data))
|
||||||
|
finally:
|
||||||
|
self.original_buffer.flush()
|
||||||
|
|
||||||
|
|
||||||
class StdinBytesIO(BytesIO):
|
class StdinBytesIO(BytesIO):
|
||||||
"""To be used for `MockEnvironment.stdin`"""
|
"""To be used for `MockEnvironment.stdin`"""
|
||||||
len = 0 # See `prepare_request_body()`
|
len = 0 # See `prepare_request_body()`
|
||||||
@ -72,17 +126,23 @@ class MockEnvironment(Environment):
|
|||||||
stdin_isatty = True
|
stdin_isatty = True
|
||||||
stdout_isatty = True
|
stdout_isatty = True
|
||||||
is_windows = False
|
is_windows = False
|
||||||
|
show_displays = False
|
||||||
|
|
||||||
def __init__(self, create_temp_config_dir=True, *, stdout_mode='b', **kwargs):
|
def __init__(self, create_temp_config_dir=True, **kwargs):
|
||||||
|
self._encoder = Encoder()
|
||||||
if 'stdout' not in kwargs:
|
if 'stdout' not in kwargs:
|
||||||
kwargs['stdout'] = tempfile.TemporaryFile(
|
kwargs['stdout'] = tempfile.NamedTemporaryFile(
|
||||||
mode=f'w+{stdout_mode}',
|
mode='w+t',
|
||||||
prefix='httpie_stdout'
|
prefix='httpie_stderr',
|
||||||
|
newline='',
|
||||||
|
encoding=UTF8,
|
||||||
)
|
)
|
||||||
|
kwargs['stdout'].buffer = FakeBytesIOBuffer(kwargs['stdout'], self._encoder)
|
||||||
if 'stderr' not in kwargs:
|
if 'stderr' not in kwargs:
|
||||||
kwargs['stderr'] = tempfile.TemporaryFile(
|
kwargs['stderr'] = tempfile.TemporaryFile(
|
||||||
mode='w+t',
|
mode='w+t',
|
||||||
prefix='httpie_stderr'
|
prefix='httpie_stderr',
|
||||||
|
encoding=UTF8,
|
||||||
)
|
)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._create_temp_config_dir = create_temp_config_dir
|
self._create_temp_config_dir = create_temp_config_dir
|
||||||
@ -143,6 +203,17 @@ class BaseCLIResponse:
|
|||||||
# pytest-httpbin to real httpbin.
|
# pytest-httpbin to real httpbin.
|
||||||
return re.sub(r'127\.0\.0\.1:\d+', 'httpbin.org', cmd)
|
return re.sub(r'127\.0\.0\.1:\d+', 'httpbin.org', cmd)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_raw_data(self, data: Union[str, bytes]) -> 'BaseCLIResponse':
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
with suppress(UnicodeDecodeError):
|
||||||
|
data = data.decode()
|
||||||
|
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
return BytesCLIResponse(data)
|
||||||
|
else:
|
||||||
|
return StrCLIResponse(data)
|
||||||
|
|
||||||
|
|
||||||
class BytesCLIResponse(bytes, BaseCLIResponse):
|
class BytesCLIResponse(bytes, BaseCLIResponse):
|
||||||
"""
|
"""
|
||||||
@ -195,7 +266,7 @@ class ExitStatusError(Exception):
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_env() -> MockEnvironment:
|
def mock_env() -> MockEnvironment:
|
||||||
env = MockEnvironment(stdout_mode='')
|
env = MockEnvironment()
|
||||||
yield env
|
yield env
|
||||||
env.cleanup()
|
env.cleanup()
|
||||||
|
|
||||||
@ -214,7 +285,7 @@ def httpie(
|
|||||||
status.
|
status.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
env = kwargs.setdefault('env', MockEnvironment(stdout_mode=''))
|
env = kwargs.setdefault('env', MockEnvironment())
|
||||||
cli_args = ['httpie']
|
cli_args = ['httpie']
|
||||||
if not kwargs.pop('no_debug', False):
|
if not kwargs.pop('no_debug', False):
|
||||||
cli_args.append('--debug')
|
cli_args.append('--debug')
|
||||||
@ -227,16 +298,7 @@ def httpie(
|
|||||||
env.stdout.seek(0)
|
env.stdout.seek(0)
|
||||||
env.stderr.seek(0)
|
env.stderr.seek(0)
|
||||||
try:
|
try:
|
||||||
output = env.stdout.read()
|
response = BaseCLIResponse.from_raw_data(env.stdout.read())
|
||||||
if isinstance(output, bytes):
|
|
||||||
with suppress(UnicodeDecodeError):
|
|
||||||
output = output.decode()
|
|
||||||
|
|
||||||
if isinstance(output, bytes):
|
|
||||||
response = BytesCLIResponse(output)
|
|
||||||
else:
|
|
||||||
response = StrCLIResponse(output)
|
|
||||||
|
|
||||||
response.stderr = env.stderr.read()
|
response.stderr = env.stderr.read()
|
||||||
response.exit_status = exit_status
|
response.exit_status = exit_status
|
||||||
response.args = cli_args
|
response.args = cli_args
|
||||||
@ -354,12 +416,11 @@ def http(
|
|||||||
devnull.seek(0)
|
devnull.seek(0)
|
||||||
output = stdout.read()
|
output = stdout.read()
|
||||||
devnull_output = devnull.read()
|
devnull_output = devnull.read()
|
||||||
try:
|
|
||||||
output = output.decode()
|
if hasattr(env, '_encoder'):
|
||||||
except UnicodeDecodeError:
|
output = env._encoder.decode(output)
|
||||||
r = BytesCLIResponse(output)
|
|
||||||
else:
|
r = BaseCLIResponse.from_raw_data(output)
|
||||||
r = StrCLIResponse(output)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
devnull_output = devnull_output.decode()
|
devnull_output = devnull_output.decode()
|
||||||
|
@ -169,7 +169,7 @@ def interface(tmp_path):
|
|||||||
|
|
||||||
return Interface(
|
return Interface(
|
||||||
path=tmp_path / 'interface',
|
path=tmp_path / 'interface',
|
||||||
environment=MockEnvironment(stdout_mode='t')
|
environment=MockEnvironment()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user