mirror of
https://github.com/httpie/cli.git
synced 2024-11-21 15:23:11 +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 .
|
||||
|
||||
|
||||
doc-update-install:
|
||||
@echo $(H1)Updating installation instructions in the docs$(H1END)
|
||||
$(VENV_PYTHON) docs/installation/generate.py
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Publishing to PyPi
|
||||
###############################################################################
|
||||
@ -211,3 +206,17 @@ brew-test:
|
||||
|
||||
@echo $(H1)Auditing…$(H1END)
|
||||
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'
|
||||
__date__ = '2022-03-08'
|
||||
__author__ = 'Jakub Roztocil'
|
||||
__licence__ = 'BSD'
|
||||
|
@ -155,6 +155,7 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
self.env = env
|
||||
self.env.args = namespace = namespace or argparse.Namespace()
|
||||
self.args, no_options = super().parse_known_args(args, namespace)
|
||||
if self.args.debug:
|
||||
self.args.traceback = True
|
||||
@ -557,19 +558,62 @@ class HTTPieArgumentParser(BaseHTTPieArgumentParser):
|
||||
parsed_options = parse_format_options(options_group, defaults=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):
|
||||
"""Prints a usage message incorporating the message to stderr and
|
||||
exits."""
|
||||
self.print_usage(sys.stderr)
|
||||
self.exit(
|
||||
2,
|
||||
self.env.rich_error_console.print(
|
||||
dedent(
|
||||
f'''
|
||||
error:
|
||||
[bold]error[/bold]:
|
||||
{message}
|
||||
|
||||
for more information:
|
||||
[bold]for more information[/bold]:
|
||||
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,
|
||||
UNSORTED_FORMAT_OPTIONS_STRING, RequestType)
|
||||
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)
|
||||
from httpie.plugins.builtin import BuiltinAuthPlugin
|
||||
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
|
||||
|
||||
options = ParserSpec(
|
||||
'http',
|
||||
description=f'{__doc__.strip()} <https://httpie.io>',
|
||||
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
|
||||
to its default value.
|
||||
|
||||
Suggestions and bug reports are greatly appreciated:
|
||||
https://github.com/httpie/httpie/issues
|
||||
""",
|
||||
@ -52,6 +56,7 @@ positional_arguments.add_argument(
|
||||
metavar='METHOD',
|
||||
nargs=Qualifiers.OPTIONAL,
|
||||
default=None,
|
||||
short_help='The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).',
|
||||
help="""
|
||||
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(
|
||||
dest='url',
|
||||
metavar='URL',
|
||||
short_help='The request URL.',
|
||||
help="""
|
||||
The scheme defaults to 'http://' if the URL does not include one.
|
||||
(You can override this with: --default-scheme=https)
|
||||
The request URL. Scheme defaults to 'http://' if the URL
|
||||
does not include one. (You can override this with: --default-scheme=http/https)
|
||||
|
||||
You can also use a shorthand for localhost
|
||||
|
||||
@ -83,6 +89,17 @@ positional_arguments.add_argument(
|
||||
nargs=Qualifiers.ZERO_OR_MORE,
|
||||
default=None,
|
||||
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"""
|
||||
Optional key-value pairs to be included in the request. The separator used
|
||||
determines the type:
|
||||
@ -136,6 +153,7 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.JSON,
|
||||
dest='request_type',
|
||||
short_help='(default) Serialize data items from the command line as a JSON object.',
|
||||
help="""
|
||||
(default) Data items from the command line are serialized as a JSON object.
|
||||
The Content-Type and Accept headers are set to application/json
|
||||
@ -149,6 +167,7 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.FORM,
|
||||
dest='request_type',
|
||||
short_help='Serialize data items from the command line as form field data.',
|
||||
help="""
|
||||
Data items from the command line are serialized as form fields.
|
||||
|
||||
@ -163,22 +182,21 @@ content_types.add_argument(
|
||||
action='store_const',
|
||||
const=RequestType.MULTIPART,
|
||||
dest='request_type',
|
||||
help="""
|
||||
Similar to --form, but always sends a multipart/form-data
|
||||
request (i.e., even without files).
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Similar to --form, but always sends a multipart/form-data '
|
||||
'request (i.e., even without files).'
|
||||
)
|
||||
)
|
||||
content_types.add_argument(
|
||||
'--boundary',
|
||||
help="""
|
||||
Specify a custom boundary string for multipart/form-data requests.
|
||||
Only has effect only together with --form.
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Specify a custom boundary string for multipart/form-data requests. '
|
||||
'Only has effect only together with --form.'
|
||||
)
|
||||
)
|
||||
content_types.add_argument(
|
||||
'--raw',
|
||||
short_help='Pass raw request data without extra processing.',
|
||||
help="""
|
||||
This option allows you to pass raw request data without extra processing
|
||||
(as opposed to the structured request items syntax):
|
||||
@ -208,6 +226,7 @@ processing_options.add_argument(
|
||||
'-x',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Compress the content with Deflate algorithm.',
|
||||
help="""
|
||||
Content compressed (encoded) with Deflate algorithm.
|
||||
The Content-Encoding header is set to deflate.
|
||||
@ -223,22 +242,33 @@ processing_options.add_argument(
|
||||
#######################################################################
|
||||
|
||||
|
||||
def format_style_help(available_styles):
|
||||
return """
|
||||
def format_style_help(available_styles, *, isolation_mode: bool = False):
|
||||
text = """
|
||||
Output coloring style (default is "{default}"). It can be one of:
|
||||
|
||||
{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.
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
""")
|
||||
|
||||
The "{auto_style}" style follows your terminal's ANSI color styles.
|
||||
For non-{auto_style} styles to work properly, please make sure that the
|
||||
$TERM environment variable is set to "xterm-256color" or similar
|
||||
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
|
||||
""".format(
|
||||
if isolation_mode:
|
||||
available_styles = sorted(BUNDLED_STYLES)
|
||||
|
||||
available_styles_text = '\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in textwrap.wrap(', '.join(available_styles), 60)
|
||||
).strip()
|
||||
return text.format(
|
||||
default=DEFAULT_STYLE,
|
||||
available_styles='\n'.join(
|
||||
f' {line.strip()}'
|
||||
for line in textwrap.wrap(', '.join(available_styles), 60)
|
||||
).strip(),
|
||||
available_styles=available_styles_text,
|
||||
auto_style=AUTO_STYLE,
|
||||
)
|
||||
|
||||
@ -261,6 +291,7 @@ output_processing.add_argument(
|
||||
dest='prettify',
|
||||
default=PRETTY_STDOUT_TTY_ONLY,
|
||||
choices=sorted(PRETTY_MAP.keys()),
|
||||
short_help='Control the processing of console outputs.',
|
||||
help="""
|
||||
Controls output processing. The value can be "none" to not prettify
|
||||
the output (default for redirected output), "all" to apply both colors
|
||||
@ -276,6 +307,7 @@ output_processing.add_argument(
|
||||
default=DEFAULT_STYLE,
|
||||
action='lazy_choices',
|
||||
getter=get_available_styles,
|
||||
short_help=f'Output coloring style (default is "{DEFAULT_STYLE}").',
|
||||
help_formatter=format_style_help,
|
||||
)
|
||||
|
||||
@ -291,6 +323,7 @@ output_processing.add_argument(
|
||||
output_processing.add_argument(
|
||||
'--unsorted',
|
||||
**_unsorted_kwargs,
|
||||
short_help='Disables all sorting while formatting output.',
|
||||
help=f"""
|
||||
Disables all sorting while formatting output. It is a shortcut for:
|
||||
|
||||
@ -301,6 +334,7 @@ output_processing.add_argument(
|
||||
output_processing.add_argument(
|
||||
'--sorted',
|
||||
**_sorted_kwargs,
|
||||
short_help='Re-enables all sorting options while formatting output.',
|
||||
help=f"""
|
||||
Re-enables all sorting options while formatting output. It is a shortcut for:
|
||||
|
||||
@ -312,6 +346,7 @@ output_processing.add_argument(
|
||||
'--response-charset',
|
||||
metavar='ENCODING',
|
||||
type=response_charset_type,
|
||||
short_help='Override the response encoding for terminal display purposes.',
|
||||
help="""
|
||||
Override the response encoding for terminal display purposes, e.g.:
|
||||
|
||||
@ -324,6 +359,7 @@ output_processing.add_argument(
|
||||
'--response-mime',
|
||||
metavar='MIME_TYPE',
|
||||
type=response_mime_type,
|
||||
short_help='Override the response mime type for coloring and formatting for the terminal.',
|
||||
help="""
|
||||
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(
|
||||
'--format-options',
|
||||
action='append',
|
||||
short_help='Controls output formatting.',
|
||||
help="""
|
||||
Controls output formatting. Only relevant when formatting is enabled
|
||||
through (explicit or implied) --pretty=all or --pretty=format.
|
||||
@ -368,6 +405,7 @@ output_options.add_argument(
|
||||
'-p',
|
||||
dest='output_options',
|
||||
metavar='WHAT',
|
||||
short_help='Options to specify what the console output should contain.',
|
||||
help=f"""
|
||||
String specifying what the output should contain:
|
||||
|
||||
@ -390,6 +428,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_HEAD,
|
||||
short_help='Print only the response headers.',
|
||||
help=f"""
|
||||
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
|
||||
|
||||
@ -401,6 +440,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_META,
|
||||
short_help='Print only the response metadata.',
|
||||
help=f"""
|
||||
Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
|
||||
|
||||
@ -412,6 +452,7 @@ output_options.add_argument(
|
||||
dest='output_options',
|
||||
action='store_const',
|
||||
const=OUT_RESP_BODY,
|
||||
short_help='Print only the response body.',
|
||||
help=f"""
|
||||
Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
|
||||
|
||||
@ -424,20 +465,22 @@ output_options.add_argument(
|
||||
dest='verbose',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Make output more verbose.',
|
||||
help=f"""
|
||||
Verbose output. For the level one (with single `-v`/`--verbose`), print
|
||||
the whole request as well as the response. Also print any intermediary
|
||||
requests/responses (such as redirects). For the second level and higher,
|
||||
print these as well as the response metadata.
|
||||
|
||||
Level one is a shortcut for: --all --print={''.join(BASE_OUTPUT_OPTIONS)}
|
||||
Level two is a shortcut for: --all --print={''.join(OUTPUT_OPTIONS)}
|
||||
Level one is a shortcut for: --all --print={''.join(sorted(BASE_OUTPUT_OPTIONS))}
|
||||
Level two is a shortcut for: --all --print={''.join(sorted(OUTPUT_OPTIONS))}
|
||||
""",
|
||||
)
|
||||
output_options.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
short_help='Show any intermediary requests/responses.',
|
||||
help="""
|
||||
By default, only the final request/response is shown. Use this flag to show
|
||||
any intermediary requests/responses as well. Intermediary requests include
|
||||
@ -451,6 +494,7 @@ output_options.add_argument(
|
||||
'-P',
|
||||
dest='output_options_history',
|
||||
metavar='WHAT',
|
||||
short_help='--print for intermediary requests/responses.',
|
||||
help="""
|
||||
The same as --print, -p but applies only to intermediary requests/responses
|
||||
(such as redirects) when their inclusion is enabled with --all. If this
|
||||
@ -464,6 +508,7 @@ output_options.add_argument(
|
||||
'-S',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Always stream the response body by line, i.e., behave like `tail -f`.',
|
||||
help="""
|
||||
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'),
|
||||
dest='output_file',
|
||||
metavar='FILE',
|
||||
short_help='Save output to FILE instead of stdout.',
|
||||
help="""
|
||||
Save output to FILE instead of stdout. If --download is also set, then only
|
||||
the response body is saved to FILE. Other parts of the HTTP exchange are
|
||||
@ -497,6 +543,7 @@ output_options.add_argument(
|
||||
'-d',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Download the body to a file instead of printing it to stdout.',
|
||||
help="""
|
||||
Do not print the response body to stdout. Rather, download it and store it
|
||||
in a file. The filename is guessed unless specified with --output
|
||||
@ -510,6 +557,7 @@ output_options.add_argument(
|
||||
dest='download_resume',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Resume an interrupted download (--output needs to be specified).',
|
||||
help="""
|
||||
Resume an interrupted download. Note that the --output option needs to be
|
||||
specified as well.
|
||||
@ -521,6 +569,7 @@ output_options.add_argument(
|
||||
'-q',
|
||||
action='count',
|
||||
default=0,
|
||||
short_help='Do not print to stdout or stderr, except for errors and warnings when provided once.',
|
||||
help="""
|
||||
Do not print to stdout or stderr, except for errors and warnings when provided once.
|
||||
Provide twice to suppress warnings as well.
|
||||
@ -544,21 +593,26 @@ sessions.add_argument(
|
||||
'--session',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
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,
|
||||
auth credential, as well as any cookies sent by the server persist between
|
||||
requests.
|
||||
|
||||
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(
|
||||
'--session-read-only',
|
||||
metavar='SESSION_NAME_OR_PATH',
|
||||
type=session_name_validator,
|
||||
short_help='Create or read a session without updating it',
|
||||
help="""
|
||||
Create or read a session without updating it form the request/response
|
||||
exchange.
|
||||
@ -571,33 +625,46 @@ sessions.add_argument(
|
||||
#######################################################################
|
||||
|
||||
|
||||
def format_auth_help(auth_plugins_mapping):
|
||||
auth_plugins = list(auth_plugins_mapping.values())
|
||||
return """
|
||||
def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
|
||||
text = """
|
||||
The authentication mechanism to be used. Defaults to "{default}".
|
||||
|
||||
{types}
|
||||
{auth_types}
|
||||
"""
|
||||
|
||||
""".format(
|
||||
auth_plugins = list(auth_plugins_mapping.values())
|
||||
if isolation_mode:
|
||||
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=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
''
|
||||
if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else f' (provided by {plugin.package_name})'
|
||||
),
|
||||
description=(
|
||||
''
|
||||
if not plugin.description
|
||||
else '\n '
|
||||
+ ('\n '.join(textwrap.wrap(plugin.description)))
|
||||
),
|
||||
)
|
||||
for plugin in auth_plugins
|
||||
)
|
||||
|
||||
return text.format(
|
||||
default=auth_plugins[0].auth_type,
|
||||
types='\n '.join(
|
||||
'"{type}": {name}{package}{description}'.format(
|
||||
type=plugin.auth_type,
|
||||
name=plugin.name,
|
||||
package=(
|
||||
''
|
||||
if issubclass(plugin, BuiltinAuthPlugin)
|
||||
else f' (provided by {plugin.package_name})'
|
||||
),
|
||||
description=(
|
||||
''
|
||||
if not plugin.description
|
||||
else '\n '
|
||||
+ ('\n '.join(textwrap.wrap(plugin.description)))
|
||||
),
|
||||
)
|
||||
for plugin in auth_plugins
|
||||
),
|
||||
auth_types=auth_types,
|
||||
)
|
||||
|
||||
|
||||
@ -608,6 +675,7 @@ authentication.add_argument(
|
||||
'-a',
|
||||
default=None,
|
||||
metavar='USER[:PASS] | TOKEN',
|
||||
short_help='Credentials for the selected (-A) authentication method.',
|
||||
help="""
|
||||
For username/password based authentication mechanisms (e.g
|
||||
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,
|
||||
sort=True,
|
||||
cache=False,
|
||||
short_help='The authentication mechanism to be used.',
|
||||
help_formatter=format_auth_help,
|
||||
)
|
||||
authentication.add_argument(
|
||||
'--ignore-netrc',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Ignore credentials from .netrc.
|
||||
|
||||
""",
|
||||
short_help='Ignore credentials from .netrc.'
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -645,9 +711,7 @@ network.add_argument(
|
||||
'--offline',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Build the request and print it but don’t actually send it.
|
||||
""",
|
||||
short_help='Build the request and print it but don’t actually send it.'
|
||||
)
|
||||
network.add_argument(
|
||||
'--proxy',
|
||||
@ -655,6 +719,7 @@ network.add_argument(
|
||||
action='append',
|
||||
metavar='PROTOCOL:PROXY_URL',
|
||||
type=KeyValueArgType(SEPARATOR_PROXY),
|
||||
short_help='String mapping of protocol to the URL of the proxy.',
|
||||
help="""
|
||||
String mapping protocol to the URL of the proxy
|
||||
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with
|
||||
@ -668,16 +733,14 @@ network.add_argument(
|
||||
'-F',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Follow 30x Location redirects.
|
||||
|
||||
""",
|
||||
short_help='Follow 30x Location redirects.'
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
'--max-redirects',
|
||||
type=int,
|
||||
default=30,
|
||||
short_help='The maximum number of redirects that should be followed (with --follow).',
|
||||
help="""
|
||||
By default, requests have a limit of 30 redirects (works with --follow).
|
||||
|
||||
@ -687,11 +750,10 @@ network.add_argument(
|
||||
'--max-headers',
|
||||
type=int,
|
||||
default=0,
|
||||
help="""
|
||||
The maximum number of response headers to be read before giving up
|
||||
(default 0, i.e., no limit).
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'The maximum number of response headers to be read before '
|
||||
'giving up (default 0, i.e., no limit).'
|
||||
)
|
||||
)
|
||||
|
||||
network.add_argument(
|
||||
@ -699,6 +761,7 @@ network.add_argument(
|
||||
type=float,
|
||||
default=0,
|
||||
metavar='SECONDS',
|
||||
short_help='The connection timeout of the request in seconds.',
|
||||
help="""
|
||||
The connection timeout of the request in seconds.
|
||||
The default value is 0, i.e., there is no timeout limit.
|
||||
@ -713,6 +776,7 @@ network.add_argument(
|
||||
'--check-status',
|
||||
default=False,
|
||||
action='store_true',
|
||||
short_help='Exit with an error status code if the server replies with an error.',
|
||||
help="""
|
||||
By default, HTTPie exits with 0 when no network or other fatal errors
|
||||
occur. This flag instructs HTTPie to also check the HTTP status code and
|
||||
@ -729,20 +793,16 @@ network.add_argument(
|
||||
'--path-as-is',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Bypass dot segment (/../ or /./) URL squashing.
|
||||
|
||||
""",
|
||||
short_help='Bypass dot segment (/../ or /./) URL squashing.'
|
||||
)
|
||||
network.add_argument(
|
||||
'--chunked',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help="""
|
||||
Enable streaming via chunked transfer encoding.
|
||||
The Transfer-Encoding header is set to chunked.
|
||||
|
||||
""",
|
||||
short_help=(
|
||||
'Enable streaming via chunked transfer encoding. '
|
||||
'The Transfer-Encoding header is set to chunked.'
|
||||
)
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -754,6 +814,7 @@ ssl = options.add_group('SSL')
|
||||
ssl.add_argument(
|
||||
'--verify',
|
||||
default='yes',
|
||||
short_help='If "no", skip SSL verification. If a file path, use it as a CA bundle.',
|
||||
help="""
|
||||
Set to "no" (or "false") to skip checking the host's SSL certificate.
|
||||
Defaults to "yes" ("true"). You can also pass the path to a CA_BUNDLE file
|
||||
@ -765,6 +826,7 @@ ssl.add_argument(
|
||||
'--ssl',
|
||||
dest='ssl_version',
|
||||
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
|
||||
short_help='The desired protocol version to used.',
|
||||
help="""
|
||||
The desired protocol version to use. This will default to
|
||||
SSL v2.3 which will negotiate the highest protocol that both
|
||||
@ -776,6 +838,7 @@ ssl.add_argument(
|
||||
)
|
||||
ssl.add_argument(
|
||||
'--ciphers',
|
||||
short_help='A string in the OpenSSL cipher list format.',
|
||||
help=f"""
|
||||
|
||||
A string in the OpenSSL cipher list format. By default, the following
|
||||
@ -789,6 +852,7 @@ ssl.add_argument(
|
||||
'--cert',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
short_help='Specifys a local cert to use as client side SSL certificate.',
|
||||
help="""
|
||||
You can specify a local cert to use as client side SSL certificate.
|
||||
This file may either contain both private key and certificate or you may
|
||||
@ -800,6 +864,7 @@ ssl.add_argument(
|
||||
'--cert-key',
|
||||
default=None,
|
||||
type=readable_file_arg,
|
||||
short_help='The private key to use with SSL. Only needed if --cert is given.',
|
||||
help="""
|
||||
The private key to use with SSL. Only needed if --cert is given and the
|
||||
certificate file does not contain the private key.
|
||||
@ -811,11 +876,12 @@ ssl.add_argument(
|
||||
'--cert-key-pass',
|
||||
default=None,
|
||||
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
|
||||
is given and the key file requires a passphrase.
|
||||
If not provided, you’ll be prompted interactively.
|
||||
'''
|
||||
"""
|
||||
)
|
||||
|
||||
#######################################################################
|
||||
@ -828,50 +894,42 @@ troubleshooting.add_argument(
|
||||
'-I',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Do not attempt to read stdin.
|
||||
|
||||
""",
|
||||
short_help='Do not attempt to read stdin'
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--help',
|
||||
action='help',
|
||||
default=Qualifiers.SUPPRESS,
|
||||
help="""
|
||||
Show this help message and exit.
|
||||
|
||||
""",
|
||||
short_help='Show this help message and exit.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--manual',
|
||||
action='manual',
|
||||
default=Qualifiers.SUPPRESS,
|
||||
short_help='Show the full manual.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help="""
|
||||
Show version and exit.
|
||||
|
||||
""",
|
||||
short_help='Show version and exit.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--traceback',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="""
|
||||
Prints the exception traceback should one occur.
|
||||
|
||||
""",
|
||||
short_help='Prints the exception traceback should one occur.',
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--default-scheme',
|
||||
default='http',
|
||||
help="""
|
||||
The default scheme to use if not specified in the URL.
|
||||
|
||||
""",
|
||||
short_help='The default scheme to use if not specified in the URL.'
|
||||
)
|
||||
troubleshooting.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
default=False,
|
||||
short_help='Print useful diagnostic information for bug reports.',
|
||||
help="""
|
||||
Prints the exception traceback should one occur, as well as other
|
||||
information useful for debugging HTTPie itself and for reporting bugs.
|
||||
|
@ -3,15 +3,16 @@ import textwrap
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
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.utils import LazyChoices
|
||||
from httpie.cli.utils import Manual, LazyChoices
|
||||
|
||||
|
||||
class Qualifiers(Enum):
|
||||
OPTIONAL = auto()
|
||||
ZERO_OR_MORE = auto()
|
||||
ONE_OR_MORE = 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'
|
||||
|
||||
|
||||
@ -69,6 +91,7 @@ class Group:
|
||||
|
||||
def add_argument(self, *args, **kwargs):
|
||||
argument = Argument(list(args), kwargs.copy())
|
||||
argument.post_init()
|
||||
self.arguments.append(argument)
|
||||
return argument
|
||||
|
||||
@ -85,14 +108,32 @@ class Argument(typing.NamedTuple):
|
||||
aliases: List[str]
|
||||
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()
|
||||
|
||||
# Unpack the dynamically computed choices, since we
|
||||
# will need to store the actual values somewhere.
|
||||
action = configuration.pop('action', None)
|
||||
short_help = configuration.pop('short_help', None)
|
||||
nested_options = configuration.pop('nested_options', None)
|
||||
|
||||
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['help'] = choices.help
|
||||
|
||||
@ -106,9 +147,13 @@ class Argument(typing.NamedTuple):
|
||||
qualifiers = JSON_QUALIFIER_TO_OPTIONS[configuration.get('nargs', Qualifiers.SUPPRESS)]
|
||||
result.update(qualifiers)
|
||||
|
||||
help_msg = configuration.get('help')
|
||||
if help_msg and help_msg is not Qualifiers.SUPPRESS:
|
||||
result['description'] = help_msg.strip()
|
||||
description = configuration.get('help')
|
||||
if description and description is not Qualifiers.SUPPRESS:
|
||||
result['short_description'] = short_help
|
||||
result['description'] = description
|
||||
|
||||
if nested_options:
|
||||
result['nested_options'] = nested_options
|
||||
|
||||
python_type = configuration.get('type')
|
||||
if python_type is not None:
|
||||
@ -123,10 +168,19 @@ class Argument(typing.NamedTuple):
|
||||
key: value
|
||||
for key, value in configuration.items()
|
||||
if key in JSON_DIRECT_MIRROR_OPTIONS
|
||||
if value is not Qualifiers.SUPPRESS
|
||||
})
|
||||
|
||||
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):
|
||||
if attribute_name in self.configuration:
|
||||
return self.configuration[attribute_name]
|
||||
@ -140,7 +194,9 @@ ARGPARSE_QUALIFIER_MAP = {
|
||||
Qualifiers.OPTIONAL: argparse.OPTIONAL,
|
||||
Qualifiers.SUPPRESS: argparse.SUPPRESS,
|
||||
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(
|
||||
@ -152,7 +208,9 @@ def to_argparse(
|
||||
description=abstract_options.description,
|
||||
epilog=abstract_options.epilog,
|
||||
)
|
||||
concrete_parser.spec = abstract_options
|
||||
concrete_parser.register('action', 'lazy_choices', LazyChoices)
|
||||
concrete_parser.register('action', 'manual', Manual)
|
||||
|
||||
for abstract_group in abstract_options.groups:
|
||||
concrete_group = concrete_parser.add_argument_group(
|
||||
@ -164,9 +222,9 @@ def to_argparse(
|
||||
for abstract_argument in abstract_group.arguments:
|
||||
concrete_group.add_argument(
|
||||
*abstract_argument.aliases,
|
||||
**map_qualifiers(
|
||||
**drop_keys(map_qualifiers(
|
||||
abstract_argument.configuration, ARGPARSE_QUALIFIER_MAP
|
||||
)
|
||||
), ARGPARSE_IGNORE_KEYS)
|
||||
)
|
||||
|
||||
return concrete_parser
|
||||
@ -181,9 +239,19 @@ JSON_DIRECT_MIRROR_OPTIONS = (
|
||||
JSON_QUALIFIER_TO_OPTIONS = {
|
||||
Qualifiers.OPTIONAL: {'is_optional': True},
|
||||
Qualifiers.ZERO_OR_MORE: {'is_optional': True, 'is_variadic': True},
|
||||
Qualifiers.ONE_OR_MORE: {'is_optional': False, 'is_variadic': True},
|
||||
Qualifiers.SUPPRESS: {}
|
||||
}
|
||||
|
||||
|
||||
def to_data(abstract_options: ParserSpec) -> Dict[str, Any]:
|
||||
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')
|
||||
|
||||
|
||||
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]):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
getter: Callable[[], Iterable[T]],
|
||||
help_formatter: Optional[Callable[[T], str]] = None,
|
||||
help_formatter: Optional[Callable[[T, bool], str]] = None,
|
||||
sort: bool = False,
|
||||
cache: bool = True,
|
||||
isolation_mode: bool = False,
|
||||
**kwargs
|
||||
) -> None:
|
||||
self.getter = getter
|
||||
self.help_formatter = help_formatter
|
||||
self.sort = sort
|
||||
self.cache = cache
|
||||
self.isolation_mode = isolation_mode
|
||||
self._help: Optional[str] = None
|
||||
self._obj: Optional[Iterable[T]] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -33,7 +56,10 @@ class LazyChoices(argparse.Action, Generic[T]):
|
||||
@property
|
||||
def help(self) -> str:
|
||||
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
|
||||
|
||||
@help.setter
|
||||
|
@ -1,9 +1,10 @@
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Iterator, IO, Optional
|
||||
from typing import Iterator, IO, Optional, TYPE_CHECKING
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@ -12,11 +13,15 @@ try:
|
||||
except ImportError:
|
||||
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 .encoding import UTF8
|
||||
|
||||
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):
|
||||
@ -40,6 +45,7 @@ class Environment:
|
||||
is used by the test suite to simulate various scenarios.
|
||||
|
||||
"""
|
||||
args = argparse.Namespace()
|
||||
is_windows: bool = is_windows
|
||||
config_dir: Path = DEFAULT_CONFIG_DIR
|
||||
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
||||
@ -52,6 +58,10 @@ class Environment:
|
||||
stderr_isatty: bool = stderr.isatty()
|
||||
colors = 256
|
||||
program_name: str = 'http'
|
||||
|
||||
# Whether to show progress bars / status spinners etc.
|
||||
show_displays: bool = True
|
||||
|
||||
if not is_windows:
|
||||
if curses:
|
||||
try:
|
||||
@ -160,3 +170,49 @@ class Environment:
|
||||
def apply_warnings_filter(self) -> None:
|
||||
if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]:
|
||||
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:
|
||||
if args.download:
|
||||
args.follow = True # --download implies --follow.
|
||||
downloader = Downloader(output_file=args.output_file, progress_file=env.stderr, resume=args.download_resume)
|
||||
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume)
|
||||
downloader.pre_request(args.headers)
|
||||
messages = collect_messages(env, args=args,
|
||||
request_body_read_callback=request_body_read_callback)
|
||||
|
@ -5,10 +5,8 @@ Download mode implementation.
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from mailbox import Message
|
||||
from time import sleep, monotonic
|
||||
from time import monotonic
|
||||
from typing import IO, Optional, Tuple
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@ -16,22 +14,11 @@ import requests
|
||||
|
||||
from .models import HTTPResponse, OutputOptions
|
||||
from .output.streams import RawStream
|
||||
from .utils import humanize_bytes
|
||||
from .context import Environment
|
||||
|
||||
|
||||
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):
|
||||
pass
|
||||
@ -176,9 +163,9 @@ class Downloader:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: Environment,
|
||||
output_file: IO = None,
|
||||
resume: bool = False,
|
||||
progress_file: IO = sys.stderr
|
||||
resume: bool = False
|
||||
):
|
||||
"""
|
||||
:param resume: Should the download resume if partial download
|
||||
@ -191,14 +178,10 @@ class Downloader:
|
||||
|
||||
"""
|
||||
self.finished = False
|
||||
self.status = DownloadStatus()
|
||||
self.status = DownloadStatus(env=env)
|
||||
self._output_file = output_file
|
||||
self._resume = resume
|
||||
self._resumed_from = 0
|
||||
self._progress_reporter = ProgressReporterThread(
|
||||
status=self.status,
|
||||
output=progress_file
|
||||
)
|
||||
|
||||
def pre_request(self, request_headers: dict):
|
||||
"""Called just before the HTTP request is sent.
|
||||
@ -261,11 +244,6 @@ class Downloader:
|
||||
except OSError:
|
||||
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)
|
||||
stream = RawStream(
|
||||
msg=HTTPResponse(final_response),
|
||||
@ -273,11 +251,11 @@ class Downloader:
|
||||
on_body_chunk_downloaded=self.chunk_downloaded,
|
||||
)
|
||||
|
||||
self._progress_reporter.output.write(
|
||||
f'Downloading {humanize_bytes(total_size) + " " if total_size is not None else ""}'
|
||||
f'to "{self._output_file.name}"\n'
|
||||
self.status.started(
|
||||
output_file=self._output_file,
|
||||
resumed_from=self._resumed_from,
|
||||
total_size=total_size
|
||||
)
|
||||
self._progress_reporter.start()
|
||||
|
||||
return stream, self._output_file
|
||||
|
||||
@ -287,7 +265,7 @@ class Downloader:
|
||||
self.status.finished()
|
||||
|
||||
def failed(self):
|
||||
self._progress_reporter.stop()
|
||||
self.status.terminate()
|
||||
|
||||
@property
|
||||
def interrupted(self) -> bool:
|
||||
@ -329,127 +307,71 @@ class Downloader:
|
||||
class DownloadStatus:
|
||||
"""Holds details about the download status."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
self.downloaded = 0
|
||||
self.total_size = None
|
||||
self.resumed_from = 0
|
||||
self.time_started = 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
|
||||
self.total_size = total_size
|
||||
self.downloaded = self.resumed_from = resumed_from
|
||||
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):
|
||||
assert self.time_finished is None
|
||||
self.downloaded += size
|
||||
self.display.update(size)
|
||||
|
||||
@property
|
||||
def has_finished(self):
|
||||
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):
|
||||
assert self.time_started is not None
|
||||
assert self.time_finished is None
|
||||
self.time_finished = monotonic()
|
||||
if hasattr(self, 'display'):
|
||||
self.display.stop(self.time_spent)
|
||||
|
||||
|
||||
class ProgressReporterThread(threading.Thread):
|
||||
"""
|
||||
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()
|
||||
def terminate(self):
|
||||
if hasattr(self, 'display'):
|
||||
self.display.stop(self.time_spent)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from textwrap import dedent
|
||||
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__
|
||||
|
||||
CLI_SESSION_UPGRADE_FLAGS = [
|
||||
@ -58,7 +59,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
||||
'or from a local paths.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
@ -66,7 +68,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
||||
'Upgrade the given plugins',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to upgrade'
|
||||
}
|
||||
],
|
||||
@ -74,7 +77,8 @@ COMMANDS['plugins'] = COMMANDS['cli']['plugins'] = {
|
||||
'Uninstall the given HTTPie plugins.',
|
||||
{
|
||||
'dest': 'targets',
|
||||
'nargs': '+',
|
||||
'metavar': 'TARGET',
|
||||
'nargs': Qualifiers.ONE_OR_MORE,
|
||||
'help': 'targets to install'
|
||||
}
|
||||
],
|
||||
@ -94,7 +98,7 @@ def missing_subcommand(*args) -> str:
|
||||
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'])
|
||||
actions = parent_parser.add_subparsers(
|
||||
dest=action_dest
|
||||
@ -107,13 +111,15 @@ def generate_subparsers(root, parent_parser, definitions):
|
||||
command_parser = actions.add_parser(command, description=descr)
|
||||
command_parser.root = root
|
||||
if is_subparser:
|
||||
generate_subparsers(root, command_parser, properties)
|
||||
generate_subparsers(root, command_parser, properties, spec)
|
||||
continue
|
||||
|
||||
group = spec.add_group(parent_parser.prog + ' ' + command, description=descr)
|
||||
for argument in properties:
|
||||
argument = argument.copy()
|
||||
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(
|
||||
@ -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.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 ...plugins import FormatterPlugin
|
||||
|
||||
|
||||
AUTO_STYLE = 'auto' # Follows terminal ANSI color styles
|
||||
DEFAULT_STYLE = AUTO_STYLE
|
||||
SOLARIZED_STYLE = 'solarized' # Bundled here
|
||||
|
||||
@ -33,7 +32,7 @@ BUNDLED_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):
|
||||
|
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_DARK = 'pie-dark'
|
||||
STYLE_PIE_LIGHT = 'pie-light'
|
||||
@ -7,8 +8,6 @@ STYLE_PIE_LIGHT = 'pie-light'
|
||||
|
||||
COLOR_PALETTE = {
|
||||
# Copy the brand palette
|
||||
'transparent': 'transparent',
|
||||
'current': 'currentColor',
|
||||
'white': '#F5F5F0',
|
||||
'black': '#1C1818',
|
||||
'grey': {
|
||||
@ -150,17 +149,27 @@ SHADE_NAMES = {
|
||||
'700': STYLE_PIE_LIGHT
|
||||
}
|
||||
|
||||
STYLE_SHADES = {
|
||||
style: shade
|
||||
for shade, style in SHADE_NAMES.items()
|
||||
}
|
||||
|
||||
SHADES = [
|
||||
'50',
|
||||
*map(str, range(100, 1000, 100))
|
||||
]
|
||||
|
||||
|
||||
def get_color(color: str, shade: str) -> Optional[str]:
|
||||
if color not in COLOR_PALETTE:
|
||||
def get_color(
|
||||
color: str,
|
||||
shade: str,
|
||||
*,
|
||||
palette: Dict[str, Dict[str, str]] = COLOR_PALETTE
|
||||
) -> Optional[str]:
|
||||
if color not in palette:
|
||||
return None
|
||||
|
||||
color_code = COLOR_PALETTE[color]
|
||||
color_code = palette[color]
|
||||
if isinstance(color_code, dict) and shade in color_code:
|
||||
return color_code[shade]
|
||||
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',
|
||||
'setuptools',
|
||||
'importlib-metadata>=1.4.0; python_version < "3.8"',
|
||||
'rich>=9.10.0'
|
||||
]
|
||||
install_requires_win_only = [
|
||||
'colorama>=0.2.4',
|
||||
@ -111,4 +112,8 @@ setup(
|
||||
'Documentation': 'https://httpie.io/docs',
|
||||
'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 shutil
|
||||
import os
|
||||
import sys
|
||||
from tests.utils import http
|
||||
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
REQUEST_ITEM_MSG = "[REQUEST_ITEM ...]"
|
||||
else:
|
||||
REQUEST_ITEM_MSG = "[REQUEST_ITEM [REQUEST_ITEM ...]]"
|
||||
|
||||
|
||||
NAKED_HELP_MESSAGE = f"""\
|
||||
NAKED_BASE_TEMPLATE = """\
|
||||
usage:
|
||||
http [METHOD] URL {REQUEST_ITEM_MSG}
|
||||
http {extra_args}[METHOD] URL [REQUEST_ITEM ...]
|
||||
|
||||
error:
|
||||
the following arguments are required: URL
|
||||
{error_msg}
|
||||
|
||||
for more information:
|
||||
run 'http --help' or visit https://httpie.io/docs/cli
|
||||
|
||||
"""
|
||||
|
||||
NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG = f"""\
|
||||
usage:
|
||||
http [--pretty {{all,colors,format,none}}] [METHOD] URL {REQUEST_ITEM_MSG}
|
||||
NAKED_HELP_MESSAGE = NAKED_BASE_TEMPLATE.format(
|
||||
extra_args="",
|
||||
error_msg="the following arguments are required: URL"
|
||||
)
|
||||
|
||||
error:
|
||||
argument --pretty: expected one argument
|
||||
NAKED_HELP_MESSAGE_PRETTY_WITH_NO_ARG = NAKED_BASE_TEMPLATE.format(
|
||||
extra_args="--pretty {all, colors, format, none} ",
|
||||
error_msg="argument --pretty: expected one argument"
|
||||
)
|
||||
|
||||
for more information:
|
||||
run 'http --help' or visit https://httpie.io/docs/cli
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
NAKED_HELP_MESSAGE_PRETTY_WITH_INVALID_ARG = NAKED_BASE_TEMPLATE.format(
|
||||
extra_args="--pretty {all, colors, format, none} ",
|
||||
error_msg="argument --pretty: invalid choice: '$invalid' (choose from 'all', 'colors', 'format', 'none')"
|
||||
)
|
||||
|
||||
|
||||
PREDEFINED_TERMINAL_SIZE = (160, 80)
|
||||
PREDEFINED_TERMINAL_SIZE = (200, 100)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -66,6 +49,7 @@ def ignore_terminal_size(monkeypatch):
|
||||
# Setting COLUMNS as an env var is required for 3.8<
|
||||
monkeypatch.setitem(os.environ, 'COLUMNS', str(PREDEFINED_TERMINAL_SIZE[0]))
|
||||
monkeypatch.setattr(shutil, 'get_terminal_size', fake_terminal_size)
|
||||
monkeypatch.setattr(os, 'get_terminal_size', fake_terminal_size)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -83,4 +83,4 @@ def test_lazy_choices_help():
|
||||
# If we use --help, then we call it with styles
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(['--help'])
|
||||
help_formatter.assert_called_once_with(['a', 'b', 'c'])
|
||||
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):
|
||||
robots_txt = '/robots.txt'
|
||||
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)
|
||||
assert 'Downloading' in r.stderr
|
||||
assert '[K' in r.stderr
|
||||
assert 'Done' in r.stderr
|
||||
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:
|
||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
||||
downloader = Downloader(mock_env, output_file=devnull)
|
||||
downloader.start(
|
||||
initial_url='/',
|
||||
final_response=Response(
|
||||
@ -148,11 +146,10 @@ class TestDownloads:
|
||||
downloader.chunk_downloaded(b'12345')
|
||||
downloader.finish()
|
||||
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:
|
||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
||||
downloader = Downloader(mock_env, output_file=devnull)
|
||||
downloader.start(
|
||||
final_response=Response(url=httpbin_both.url + '/'),
|
||||
initial_url='/'
|
||||
@ -161,15 +158,14 @@ class TestDownloads:
|
||||
downloader.chunk_downloaded(b'12345')
|
||||
downloader.finish()
|
||||
assert not downloader.interrupted
|
||||
downloader._progress_reporter.join()
|
||||
|
||||
def test_download_output_from_content_disposition(self, httpbin_both):
|
||||
with tempfile.TemporaryDirectory() as tmp_dirname, open(os.devnull, 'w') as devnull:
|
||||
def test_download_output_from_content_disposition(self, mock_env, httpbin_both):
|
||||
with tempfile.TemporaryDirectory() as tmp_dirname:
|
||||
orig_cwd = os.getcwd()
|
||||
os.chdir(tmp_dirname)
|
||||
try:
|
||||
assert not os.path.isfile('filename.bin')
|
||||
downloader = Downloader(progress_file=devnull)
|
||||
downloader = Downloader(mock_env)
|
||||
downloader.start(
|
||||
final_response=Response(
|
||||
url=httpbin_both.url + '/',
|
||||
@ -184,7 +180,6 @@ class TestDownloads:
|
||||
downloader.finish()
|
||||
downloader.failed() # Stop the reporter
|
||||
assert not downloader.interrupted
|
||||
downloader._progress_reporter.join()
|
||||
|
||||
# TODO: Auto-close the file in that case?
|
||||
downloader._output_file.close()
|
||||
@ -192,9 +187,9 @@ class TestDownloads:
|
||||
finally:
|
||||
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:
|
||||
downloader = Downloader(output_file=devnull, progress_file=devnull)
|
||||
downloader = Downloader(mock_env, output_file=devnull)
|
||||
downloader.start(
|
||||
final_response=Response(
|
||||
url=httpbin_both.url + '/',
|
||||
@ -205,17 +200,16 @@ class TestDownloads:
|
||||
downloader.chunk_downloaded(b'1234')
|
||||
downloader.finish()
|
||||
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:
|
||||
file = os.path.join(tmp_dirname, 'file.bin')
|
||||
with open(file, 'a'):
|
||||
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
|
||||
downloader = Downloader(output_file=output_file, progress_file=devnull)
|
||||
downloader = Downloader(mock_env, output_file=output_file)
|
||||
downloader.start(
|
||||
final_response=Response(
|
||||
url=httpbin_both.url + '/',
|
||||
@ -227,15 +221,14 @@ class TestDownloads:
|
||||
downloader.finish()
|
||||
downloader.failed()
|
||||
assert downloader.interrupted
|
||||
downloader._progress_reporter.join()
|
||||
|
||||
# Write bytes
|
||||
with open(file, 'wb') as fh:
|
||||
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
|
||||
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
|
||||
headers = {}
|
||||
@ -253,7 +246,6 @@ class TestDownloads:
|
||||
)
|
||||
downloader.chunk_downloaded(b'45')
|
||||
downloader.finish()
|
||||
downloader._progress_reporter.join()
|
||||
|
||||
def test_download_with_redirect_original_url_used_for_filename(self, httpbin):
|
||||
# Redirect from `/redirect/1` to `/get`.
|
||||
|
@ -97,6 +97,8 @@ class TestQuietFlag:
|
||||
(['-q'], 1),
|
||||
(['-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 warn_and_run(*args, **kwargs):
|
||||
warnings.warn('warning!!')
|
||||
|
@ -5,19 +5,20 @@ def test_parser_serialization():
|
||||
small_parser = ParserSpec("test_parser")
|
||||
|
||||
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(
|
||||
"variadic_arg",
|
||||
metavar="META",
|
||||
help=Qualifiers.SUPPRESS,
|
||||
nargs=Qualifiers.ZERO_OR_MORE,
|
||||
nargs=Qualifiers.ZERO_OR_MORE
|
||||
)
|
||||
group_1.add_argument(
|
||||
"-O",
|
||||
"--opt-arg",
|
||||
action="lazy_choices",
|
||||
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")
|
||||
@ -36,6 +37,7 @@ def test_parser_serialization():
|
||||
{
|
||||
"options": ["regular_arg"],
|
||||
"description": "regular arg",
|
||||
"short_description": "short",
|
||||
},
|
||||
{
|
||||
"options": ["variadic_arg"],
|
||||
@ -46,6 +48,7 @@ def test_parser_serialization():
|
||||
{
|
||||
"options": ["-O", "--opt-arg"],
|
||||
"description": "opt_1, opt_2",
|
||||
"short_description": "short_help",
|
||||
"choices": ["opt_1", "opt_2"],
|
||||
},
|
||||
],
|
||||
|
@ -136,7 +136,7 @@ def test_auto_streaming(http_server, extras, expected):
|
||||
assert len([
|
||||
call_arg
|
||||
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
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ import httpie.manager.__main__ as manager
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.config import Config
|
||||
from httpie.encoding import UTF8
|
||||
from httpie.context import Environment
|
||||
from httpie.utils import url_as_host
|
||||
|
||||
@ -61,6 +62,59 @@ def add_auth(url, auth):
|
||||
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):
|
||||
"""To be used for `MockEnvironment.stdin`"""
|
||||
len = 0 # See `prepare_request_body()`
|
||||
@ -72,17 +126,23 @@ class MockEnvironment(Environment):
|
||||
stdin_isatty = True
|
||||
stdout_isatty = True
|
||||
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:
|
||||
kwargs['stdout'] = tempfile.TemporaryFile(
|
||||
mode=f'w+{stdout_mode}',
|
||||
prefix='httpie_stdout'
|
||||
kwargs['stdout'] = tempfile.NamedTemporaryFile(
|
||||
mode='w+t',
|
||||
prefix='httpie_stderr',
|
||||
newline='',
|
||||
encoding=UTF8,
|
||||
)
|
||||
kwargs['stdout'].buffer = FakeBytesIOBuffer(kwargs['stdout'], self._encoder)
|
||||
if 'stderr' not in kwargs:
|
||||
kwargs['stderr'] = tempfile.TemporaryFile(
|
||||
mode='w+t',
|
||||
prefix='httpie_stderr'
|
||||
prefix='httpie_stderr',
|
||||
encoding=UTF8,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
self._create_temp_config_dir = create_temp_config_dir
|
||||
@ -143,6 +203,17 @@ class BaseCLIResponse:
|
||||
# pytest-httpbin to real httpbin.
|
||||
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):
|
||||
"""
|
||||
@ -195,7 +266,7 @@ class ExitStatusError(Exception):
|
||||
|
||||
@pytest.fixture
|
||||
def mock_env() -> MockEnvironment:
|
||||
env = MockEnvironment(stdout_mode='')
|
||||
env = MockEnvironment()
|
||||
yield env
|
||||
env.cleanup()
|
||||
|
||||
@ -214,7 +285,7 @@ def httpie(
|
||||
status.
|
||||
"""
|
||||
|
||||
env = kwargs.setdefault('env', MockEnvironment(stdout_mode=''))
|
||||
env = kwargs.setdefault('env', MockEnvironment())
|
||||
cli_args = ['httpie']
|
||||
if not kwargs.pop('no_debug', False):
|
||||
cli_args.append('--debug')
|
||||
@ -227,16 +298,7 @@ def httpie(
|
||||
env.stdout.seek(0)
|
||||
env.stderr.seek(0)
|
||||
try:
|
||||
output = 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 = BaseCLIResponse.from_raw_data(env.stdout.read())
|
||||
response.stderr = env.stderr.read()
|
||||
response.exit_status = exit_status
|
||||
response.args = cli_args
|
||||
@ -354,12 +416,11 @@ def http(
|
||||
devnull.seek(0)
|
||||
output = stdout.read()
|
||||
devnull_output = devnull.read()
|
||||
try:
|
||||
output = output.decode()
|
||||
except UnicodeDecodeError:
|
||||
r = BytesCLIResponse(output)
|
||||
else:
|
||||
r = StrCLIResponse(output)
|
||||
|
||||
if hasattr(env, '_encoder'):
|
||||
output = env._encoder.decode(output)
|
||||
|
||||
r = BaseCLIResponse.from_raw_data(output)
|
||||
|
||||
try:
|
||||
devnull_output = devnull_output.decode()
|
||||
|
@ -169,7 +169,7 @@ def interface(tmp_path):
|
||||
|
||||
return Interface(
|
||||
path=tmp_path / 'interface',
|
||||
environment=MockEnvironment(stdout_mode='t')
|
||||
environment=MockEnvironment()
|
||||
)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user