Parse and pass request body (#15)

* Handle and skip empty request segments

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>

* Fix splitting of requests

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>

* Parse and pass request body

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>

* Format files

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>

* Format definition.py to follow code style

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>

---------

Co-authored-by: Jakub Rybak <laykos0@protonmail.com>
This commit is contained in:
Elias Floreteng 2025-03-06 16:22:23 +01:00 committed by GitHub
parent b4327fae07
commit 4481bfb332
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 414 additions and 366 deletions

View File

@ -5,20 +5,39 @@ import textwrap
from argparse import FileType from argparse import FileType
from httpie import __doc__, __version__ from httpie import __doc__, __version__
from httpie.cli.argtypes import (KeyValueArgType, SessionNameValidator, from httpie.cli.argtypes import (
SSLCredentials, readable_file_arg, KeyValueArgType,
response_charset_type, response_mime_type) SessionNameValidator,
from httpie.cli.constants import (BASE_OUTPUT_OPTIONS, DEFAULT_FORMAT_OPTIONS, SSLCredentials,
OUT_REQ_BODY, OUT_REQ_HEAD, OUT_RESP_BODY, readable_file_arg,
OUT_RESP_HEAD, OUT_RESP_META, OUTPUT_OPTIONS, response_charset_type,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP, response_mime_type,
PRETTY_STDOUT_TTY_ONLY, )
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, from httpie.cli.constants import (
SORTED_FORMAT_OPTIONS_STRING, BASE_OUTPUT_OPTIONS,
UNSORTED_FORMAT_OPTIONS_STRING, RequestType) DEFAULT_FORMAT_OPTIONS,
OUT_REQ_BODY,
OUT_REQ_HEAD,
OUT_RESP_BODY,
OUT_RESP_HEAD,
OUT_RESP_META,
OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT,
PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY,
SEPARATOR_GROUP_ALL_ITEMS,
SEPARATOR_PROXY,
SORTED_FORMAT_OPTIONS_STRING,
UNSORTED_FORMAT_OPTIONS_STRING,
RequestType,
)
from httpie.cli.options import ParserSpec, Qualifiers, to_argparse from httpie.cli.options import ParserSpec, Qualifiers, to_argparse
from httpie.output.formatters.colors import (AUTO_STYLE, DEFAULT_STYLE, BUNDLED_STYLES, from httpie.output.formatters.colors import (
get_available_styles) AUTO_STYLE,
DEFAULT_STYLE,
BUNDLED_STYLES,
get_available_styles,
)
from httpie.plugins.builtin import BuiltinAuthPlugin from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.plugins.registry import plugin_manager from httpie.plugins.registry import plugin_manager
from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_STRING from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_STRING
@ -26,12 +45,12 @@ from httpie.ssl_ import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS_S
# Man pages are static (built when making a release). # Man pages are static (built when making a release).
# We use this check to not include generated, system-specific information there (e.g., default --ciphers). # We use this check to not include generated, system-specific information there (e.g., default --ciphers).
IS_MAN_PAGE = bool(os.environ.get('HTTPIE_BUILDING_MAN_PAGES')) IS_MAN_PAGE = bool(os.environ.get("HTTPIE_BUILDING_MAN_PAGES"))
options = ParserSpec( options = ParserSpec(
'http', "http",
description=f'{__doc__.strip()} <https://httpie.io>', description=f"{__doc__.strip()} <https://httpie.io>",
epilog=""" epilog="""
For every --OPTION there is also a --no-OPTION that reverts OPTION For every --OPTION there is also a --no-OPTION that reverts OPTION
to its default value. to its default value.
@ -39,7 +58,7 @@ options = ParserSpec(
Suggestions and bug reports are greatly appreciated: Suggestions and bug reports are greatly appreciated:
https://github.com/httpie/cli/issues https://github.com/httpie/cli/issues
""", """,
source_file=__file__ source_file=__file__,
) )
####################################################################### #######################################################################
@ -47,7 +66,7 @@ options = ParserSpec(
####################################################################### #######################################################################
positional_arguments = options.add_group( positional_arguments = options.add_group(
'Positional arguments', "Positional arguments",
description=""" description="""
These arguments come after any flags and in the order they are listed here. These arguments come after any flags and in the order they are listed here.
Only URL is required. Only URL is required.
@ -55,11 +74,11 @@ positional_arguments = options.add_group(
) )
positional_arguments.add_argument( positional_arguments.add_argument(
dest='method', dest="method",
metavar='METHOD', metavar="METHOD",
nargs=Qualifiers.OPTIONAL, nargs=Qualifiers.OPTIONAL,
default=None, default=None,
short_help='The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).', short_help="The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).",
help=""" help="""
The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...). The HTTP method to be used for the request (GET, POST, PUT, DELETE, ...).
@ -72,9 +91,9 @@ positional_arguments.add_argument(
""", """,
) )
positional_arguments.add_argument( positional_arguments.add_argument(
dest='url', dest="url",
metavar='URL', metavar="URL",
short_help='The request URL.', short_help="The request URL.",
help=""" help="""
The request URL. Scheme defaults to 'http://' if the URL The request URL. Scheme defaults to 'http://' if the URL
does not include one. (You can override this with: --default-scheme=http/https) does not include one. (You can override this with: --default-scheme=http/https)
@ -87,21 +106,29 @@ positional_arguments.add_argument(
""", """,
) )
positional_arguments.add_argument( positional_arguments.add_argument(
dest='request_items', dest="request_items",
metavar='REQUEST_ITEM', metavar="REQUEST_ITEM",
nargs=Qualifiers.ZERO_OR_MORE, nargs=Qualifiers.ZERO_OR_MORE,
default=None, default=None,
type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS), type=KeyValueArgType(*SEPARATOR_GROUP_ALL_ITEMS),
short_help=( short_help=(
'HTTPies request items syntax for specifying HTTP headers, JSON/Form' "HTTPies request items syntax for specifying HTTP headers, JSON/Form"
'data, files, and URL parameters.' "data, files, and URL parameters."
), ),
nested_options=[ nested_options=[
('HTTP Headers', 'Name:Value', 'Arbitrary HTTP header, e.g X-API-Token:123'), ("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)'), "URL Parameters",
('Raw JSON Fields', 'field:=json', 'Data field for real JSON types.'), "name==value",
('File upload Fields', 'field@/dir/file', 'Path field for uploading a file.'), "Querystring parameter to the URL, e.g limit==50",
),
(
"Data Fields",
"field=value",
"Data fields to be serialized as JSON (default) or Form Data (with --form)",
),
("Raw JSON Fields", "field:=json", "Data field for real JSON types."),
("File upload Fields", "field@/dir/file", "Path field for uploading a file."),
], ],
help=r""" help=r"""
Optional key-value pairs to be included in the request. The separator used Optional key-value pairs to be included in the request. The separator used
@ -148,15 +175,15 @@ positional_arguments.add_argument(
# Content type. # Content type.
####################################################################### #######################################################################
content_types = options.add_group('Predefined content types') content_types = options.add_group("Predefined content types")
content_types.add_argument( content_types.add_argument(
'--json', "--json",
'-j', "-j",
action='store_const', action="store_const",
const=RequestType.JSON, const=RequestType.JSON,
dest='request_type', dest="request_type",
short_help='(default) Serialize data items from the command line as a JSON object.', short_help="(default) Serialize data items from the command line as a JSON object.",
help=""" help="""
(default) Data items from the command line are serialized as a JSON object. (default) Data items from the command line are serialized as a JSON object.
The Content-Type and Accept headers are set to application/json The Content-Type and Accept headers are set to application/json
@ -165,12 +192,12 @@ content_types.add_argument(
""", """,
) )
content_types.add_argument( content_types.add_argument(
'--form', "--form",
'-f', "-f",
action='store_const', action="store_const",
const=RequestType.FORM, const=RequestType.FORM,
dest='request_type', dest="request_type",
short_help='Serialize data items from the command line as form field data.', short_help="Serialize data items from the command line as form field data.",
help=""" help="""
Data items from the command line are serialized as form fields. Data items from the command line are serialized as form fields.
@ -181,25 +208,25 @@ content_types.add_argument(
""", """,
) )
content_types.add_argument( content_types.add_argument(
'--multipart', "--multipart",
action='store_const', action="store_const",
const=RequestType.MULTIPART, const=RequestType.MULTIPART,
dest='request_type', dest="request_type",
short_help=( short_help=(
'Similar to --form, but always sends a multipart/form-data ' "Similar to --form, but always sends a multipart/form-data "
'request (i.e., even without files).' "request (i.e., even without files)."
) ),
) )
content_types.add_argument( content_types.add_argument(
'--boundary', "--boundary",
short_help=( short_help=(
'Specify a custom boundary string for multipart/form-data requests. ' "Specify a custom boundary string for multipart/form-data requests. "
'Only has effect only together with --form.' "Only has effect only together with --form."
) ),
) )
content_types.add_argument( content_types.add_argument(
'--raw', "--raw",
short_help='Pass raw request data without extra processing.', short_help="Pass raw request data without extra processing.",
help=""" help="""
This option allows you to pass raw request data without extra processing This option allows you to pass raw request data without extra processing
(as opposed to the structured request items syntax): (as opposed to the structured request items syntax):
@ -234,14 +261,14 @@ content_types.add_argument(
# Content processing. # Content processing.
####################################################################### #######################################################################
processing_options = options.add_group('Content processing options') processing_options = options.add_group("Content processing options")
processing_options.add_argument( processing_options.add_argument(
'--compress', "--compress",
'-x', "-x",
action='count', action="count",
default=0, default=0,
short_help='Compress the content with Deflate algorithm.', short_help="Compress the content with Deflate algorithm.",
help=""" help="""
Content compressed (encoded) with Deflate algorithm. Content compressed (encoded) with Deflate algorithm.
The Content-Encoding header is set to deflate. The Content-Encoding header is set to deflate.
@ -265,9 +292,9 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
{available_styles} {available_styles}
""" """
if isolation_mode: if isolation_mode:
text += '\n\n' text += "\n\n"
text += 'For finding out all available styles in your system, try:\n\n' text += "For finding out all available styles in your system, try:\n\n"
text += ' $ http --style\n' text += " $ http --style\n"
text += textwrap.dedent(""" text += textwrap.dedent("""
The "{auto_style}" style follows your terminal's ANSI color styles. The "{auto_style}" style follows your terminal's ANSI color styles.
For non-{auto_style} styles to work properly, please make sure that the For non-{auto_style} styles to work properly, please make sure that the
@ -278,9 +305,8 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
if isolation_mode: if isolation_mode:
available_styles = sorted(BUNDLED_STYLES) available_styles = sorted(BUNDLED_STYLES)
available_styles_text = '\n'.join( available_styles_text = "\n".join(
f' {line.strip()}' f" {line.strip()}" for line in textwrap.wrap(", ".join(available_styles), 60)
for line in textwrap.wrap(', '.join(available_styles), 60)
).strip() ).strip()
return text.format( return text.format(
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
@ -290,24 +316,24 @@ def format_style_help(available_styles, *, isolation_mode: bool = False):
_sorted_kwargs = { _sorted_kwargs = {
'action': 'append_const', "action": "append_const",
'const': SORTED_FORMAT_OPTIONS_STRING, "const": SORTED_FORMAT_OPTIONS_STRING,
'dest': 'format_options', "dest": "format_options",
} }
_unsorted_kwargs = { _unsorted_kwargs = {
'action': 'append_const', "action": "append_const",
'const': UNSORTED_FORMAT_OPTIONS_STRING, "const": UNSORTED_FORMAT_OPTIONS_STRING,
'dest': 'format_options', "dest": "format_options",
} }
output_processing = options.add_group('Output processing') output_processing = options.add_group("Output processing")
output_processing.add_argument( output_processing.add_argument(
'--pretty', "--pretty",
dest='prettify', dest="prettify",
default=PRETTY_STDOUT_TTY_ONLY, default=PRETTY_STDOUT_TTY_ONLY,
choices=sorted(PRETTY_MAP.keys()), choices=sorted(PRETTY_MAP.keys()),
short_help='Control the processing of console outputs.', short_help="Control the processing of console outputs.",
help=""" help="""
Controls output processing. The value can be "none" to not prettify Controls output processing. The value can be "none" to not prettify
the output (default for redirected output), "all" to apply both colors the output (default for redirected output), "all" to apply both colors
@ -316,12 +342,12 @@ output_processing.add_argument(
""", """,
) )
output_processing.add_argument( output_processing.add_argument(
'--style', "--style",
'-s', "-s",
dest='style', dest="style",
metavar='STYLE', metavar="STYLE",
default=DEFAULT_STYLE, default=DEFAULT_STYLE,
action='lazy_choices', action="lazy_choices",
getter=get_available_styles, getter=get_available_styles,
short_help=f'Output coloring style (default is "{DEFAULT_STYLE}").', short_help=f'Output coloring style (default is "{DEFAULT_STYLE}").',
help_formatter=format_style_help, help_formatter=format_style_help,
@ -330,16 +356,16 @@ output_processing.add_argument(
# The closest approx. of the documented resetting to default via --no-<option>. # The closest approx. of the documented resetting to default via --no-<option>.
# We hide them from the doc because they act only as low-level aliases here. # We hide them from the doc because they act only as low-level aliases here.
output_processing.add_argument( output_processing.add_argument(
'--no-unsorted', **_sorted_kwargs, help=Qualifiers.SUPPRESS "--no-unsorted", **_sorted_kwargs, help=Qualifiers.SUPPRESS
) )
output_processing.add_argument( output_processing.add_argument(
'--no-sorted', **_unsorted_kwargs, help=Qualifiers.SUPPRESS "--no-sorted", **_unsorted_kwargs, help=Qualifiers.SUPPRESS
) )
output_processing.add_argument( output_processing.add_argument(
'--unsorted', "--unsorted",
**_unsorted_kwargs, **_unsorted_kwargs,
short_help='Disables all sorting while formatting output.', short_help="Disables all sorting while formatting output.",
help=f""" help=f"""
Disables all sorting while formatting output. It is a shortcut for: Disables all sorting while formatting output. It is a shortcut for:
@ -348,9 +374,9 @@ output_processing.add_argument(
""", """,
) )
output_processing.add_argument( output_processing.add_argument(
'--sorted', "--sorted",
**_sorted_kwargs, **_sorted_kwargs,
short_help='Re-enables all sorting options while formatting output.', short_help="Re-enables all sorting options while formatting output.",
help=f""" help=f"""
Re-enables all sorting options while formatting output. It is a shortcut for: Re-enables all sorting options while formatting output. It is a shortcut for:
@ -359,10 +385,10 @@ output_processing.add_argument(
""", """,
) )
output_processing.add_argument( output_processing.add_argument(
'--response-charset', "--response-charset",
metavar='ENCODING', metavar="ENCODING",
type=response_charset_type, type=response_charset_type,
short_help='Override the response encoding for terminal display purposes.', short_help="Override the response encoding for terminal display purposes.",
help=""" help="""
Override the response encoding for terminal display purposes, e.g.: Override the response encoding for terminal display purposes, e.g.:
@ -372,10 +398,10 @@ output_processing.add_argument(
""", """,
) )
output_processing.add_argument( output_processing.add_argument(
'--response-mime', "--response-mime",
metavar='MIME_TYPE', metavar="MIME_TYPE",
type=response_mime_type, type=response_mime_type,
short_help='Override the response mime type for coloring and formatting for the terminal.', short_help="Override the response mime type for coloring and formatting for the terminal.",
help=""" help="""
Override the response mime type for coloring and formatting for the terminal, e.g.: Override the response mime type for coloring and formatting for the terminal, e.g.:
@ -385,9 +411,9 @@ output_processing.add_argument(
""", """,
) )
output_processing.add_argument( output_processing.add_argument(
'--format-options', "--format-options",
action='append', action="append",
short_help='Controls output formatting.', short_help="Controls output formatting.",
help=""" help="""
Controls output formatting. Only relevant when formatting is enabled Controls output formatting. Only relevant when formatting is enabled
through (explicit or implied) --pretty=all or --pretty=format. through (explicit or implied) --pretty=all or --pretty=format.
@ -404,8 +430,8 @@ output_processing.add_argument(
This is something you will typically put into your config file. This is something you will typically put into your config file.
""".format( """.format(
option_list='\n'.join( option_list="\n".join(
f' {option}' for option in DEFAULT_FORMAT_OPTIONS f" {option}" for option in DEFAULT_FORMAT_OPTIONS
).strip() ).strip()
), ),
) )
@ -414,14 +440,14 @@ output_processing.add_argument(
# Output options # Output options
####################################################################### #######################################################################
output_options = options.add_group('Output options') output_options = options.add_group("Output options")
output_options.add_argument( output_options.add_argument(
'--print', "--print",
'-p', "-p",
dest='output_options', dest="output_options",
metavar='WHAT', metavar="WHAT",
short_help='Options to specify what the console output should contain.', short_help="Options to specify what the console output should contain.",
help=f""" help=f"""
String specifying what the output should contain: String specifying what the output should contain:
@ -439,36 +465,36 @@ output_options.add_argument(
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--headers', "--headers",
'-h', "-h",
dest='output_options', dest="output_options",
action='store_const', action="store_const",
const=OUT_RESP_HEAD, const=OUT_RESP_HEAD,
short_help='Print only the response headers.', short_help="Print only the response headers.",
help=f""" help=f"""
Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}. Print only the response headers. Shortcut for --print={OUT_RESP_HEAD}.
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--meta', "--meta",
'-m', "-m",
dest='output_options', dest="output_options",
action='store_const', action="store_const",
const=OUT_RESP_META, const=OUT_RESP_META,
short_help='Print only the response metadata.', short_help="Print only the response metadata.",
help=f""" help=f"""
Print only the response metadata. Shortcut for --print={OUT_RESP_META}. Print only the response metadata. Shortcut for --print={OUT_RESP_META}.
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--body', "--body",
'-b', "-b",
dest='output_options', dest="output_options",
action='store_const', action="store_const",
const=OUT_RESP_BODY, const=OUT_RESP_BODY,
short_help='Print only the response body.', short_help="Print only the response body.",
help=f""" help=f"""
Print only the response body. Shortcut for --print={OUT_RESP_BODY}. Print only the response body. Shortcut for --print={OUT_RESP_BODY}.
@ -476,27 +502,27 @@ output_options.add_argument(
) )
output_options.add_argument( output_options.add_argument(
'--verbose', "--verbose",
'-v', "-v",
dest='verbose', dest="verbose",
action='count', action="count",
default=0, default=0,
short_help='Make output more verbose.', short_help="Make output more verbose.",
help=f""" help=f"""
Verbose output. For the level one (with single `-v`/`--verbose`), print Verbose output. For the level one (with single `-v`/`--verbose`), print
the whole request as well as the response. Also print any intermediary the whole request as well as the response. Also print any intermediary
requests/responses (such as redirects). For the second level and higher, requests/responses (such as redirects). For the second level and higher,
print these as well as the response metadata. print these as well as the response metadata.
Level one is a shortcut for: --all --print={''.join(sorted(BASE_OUTPUT_OPTIONS))} Level one is a shortcut for: --all --print={"".join(sorted(BASE_OUTPUT_OPTIONS))}
Level two is a shortcut for: --all --print={''.join(sorted(OUTPUT_OPTIONS))} Level two is a shortcut for: --all --print={"".join(sorted(OUTPUT_OPTIONS))}
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--all', "--all",
default=False, default=False,
action='store_true', action="store_true",
short_help='Show any intermediary requests/responses.', short_help="Show any intermediary requests/responses.",
help=""" help="""
By default, only the final request/response is shown. Use this flag to show By default, only the final request/response is shown. Use this flag to show
any intermediary requests/responses as well. Intermediary requests include any intermediary requests/responses as well. Intermediary requests include
@ -506,18 +532,18 @@ output_options.add_argument(
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--history-print', "--history-print",
'-P', "-P",
dest='output_options_history', dest="output_options_history",
metavar='WHAT', metavar="WHAT",
help=Qualifiers.SUPPRESS, help=Qualifiers.SUPPRESS,
) )
output_options.add_argument( output_options.add_argument(
'--stream', "--stream",
'-S', "-S",
action='store_true', action="store_true",
default=False, default=False,
short_help='Always stream the response body by line, i.e., behave like `tail -f`.', short_help="Always stream the response body by line, i.e., behave like `tail -f`.",
help=""" help="""
Always stream the response body by line, i.e., behave like `tail -f'. Always stream the response body by line, i.e., behave like `tail -f'.
@ -533,12 +559,12 @@ output_options.add_argument(
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--output', "--output",
'-o', "-o",
type=FileType('a+b'), type=FileType("a+b"),
dest='output_file', dest="output_file",
metavar='FILE', metavar="FILE",
short_help='Save output to FILE instead of stdout.', short_help="Save output to FILE instead of stdout.",
help=""" help="""
Save output to FILE instead of stdout. If --download is also set, then only Save output to FILE instead of stdout. If --download is also set, then only
the response body is saved to FILE. Other parts of the HTTP exchange are the response body is saved to FILE. Other parts of the HTTP exchange are
@ -548,11 +574,11 @@ output_options.add_argument(
) )
output_options.add_argument( output_options.add_argument(
'--download', "--download",
'-d', "-d",
action='store_true', action="store_true",
default=False, default=False,
short_help='Download the body to a file instead of printing it to stdout.', short_help="Download the body to a file instead of printing it to stdout.",
help=""" help="""
Do not print the response body to stdout. Rather, download it and store it Do not print the response body to stdout. Rather, download it and store it
in a file. The filename is guessed unless specified with --output in a file. The filename is guessed unless specified with --output
@ -561,12 +587,12 @@ output_options.add_argument(
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--continue', "--continue",
'-c', "-c",
dest='download_resume', dest="download_resume",
action='store_true', action="store_true",
default=False, default=False,
short_help='Resume an interrupted download (--output needs to be specified).', short_help="Resume an interrupted download (--output needs to be specified).",
help=""" help="""
Resume an interrupted download. Note that the --output option needs to be Resume an interrupted download. Note that the --output option needs to be
specified as well. specified as well.
@ -574,11 +600,11 @@ output_options.add_argument(
""", """,
) )
output_options.add_argument( output_options.add_argument(
'--quiet', "--quiet",
'-q', "-q",
action='count', action="count",
default=0, default=0,
short_help='Do not print to stdout or stderr, except for errors and warnings when provided once.', short_help="Do not print to stdout or stderr, except for errors and warnings when provided once.",
help=""" help="""
Do not print to stdout or stderr, except for errors and warnings when provided once. Do not print to stdout or stderr, except for errors and warnings when provided once.
Provide twice to suppress warnings as well. Provide twice to suppress warnings as well.
@ -593,16 +619,16 @@ output_options.add_argument(
####################################################################### #######################################################################
session_name_validator = SessionNameValidator( session_name_validator = SessionNameValidator(
'Session name contains invalid characters.' "Session name contains invalid characters."
) )
sessions = options.add_group('Sessions', is_mutually_exclusive=True) sessions = options.add_group("Sessions", is_mutually_exclusive=True)
sessions.add_argument( sessions.add_argument(
'--session', "--session",
metavar='SESSION_NAME_OR_PATH', metavar="SESSION_NAME_OR_PATH",
type=session_name_validator, type=session_name_validator,
short_help='Create, or reuse and update a session.', short_help="Create, or reuse and update a session.",
help=""" help="""
Create, or reuse and update a session. Within a session, custom headers, Create, or reuse and update a session. Within a session, custom headers,
auth credential, as well as any cookies sent by the server persist between auth credential, as well as any cookies sent by the server persist between
@ -618,10 +644,10 @@ sessions.add_argument(
""", """,
) )
sessions.add_argument( sessions.add_argument(
'--session-read-only', "--session-read-only",
metavar='SESSION_NAME_OR_PATH', metavar="SESSION_NAME_OR_PATH",
type=session_name_validator, type=session_name_validator,
short_help='Create or read a session without updating it', short_help="Create or read a session without updating it",
help=""" help="""
Create or read a session without updating it form the request/response Create or read a session without updating it form the request/response
exchange. exchange.
@ -649,24 +675,23 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
for auth_plugin in auth_plugins for auth_plugin in auth_plugins
if issubclass(auth_plugin, BuiltinAuthPlugin) if issubclass(auth_plugin, BuiltinAuthPlugin)
] ]
text += '\n' text += "\n"
text += 'To see all available auth types on your system, including ones installed via plugins, run:\n\n' text += "To see all available auth types on your system, including ones installed via plugins, run:\n\n"
text += ' $ http --auth-type' text += " $ http --auth-type"
auth_types = '\n\n '.join( auth_types = "\n\n ".join(
'"{type}": {name}{package}{description}'.format( '"{type}": {name}{package}{description}'.format(
type=plugin.auth_type, type=plugin.auth_type,
name=plugin.name, name=plugin.name,
package=( package=(
'' ""
if issubclass(plugin, BuiltinAuthPlugin) if issubclass(plugin, BuiltinAuthPlugin)
else f' (provided by {plugin.package_name})' else f" (provided by {plugin.package_name})"
), ),
description=( description=(
'' ""
if not plugin.description if not plugin.description
else '\n ' else "\n " + ("\n ".join(textwrap.wrap(plugin.description)))
+ ('\n '.join(textwrap.wrap(plugin.description)))
), ),
) )
for plugin in auth_plugins for plugin in auth_plugins
@ -678,14 +703,14 @@ def format_auth_help(auth_plugins_mapping, *, isolation_mode: bool = False):
) )
authentication = options.add_group('Authentication') authentication = options.add_group("Authentication")
authentication.add_argument( authentication.add_argument(
'--auth', "--auth",
'-a', "-a",
default=None, default=None,
metavar='USER[:PASS] | TOKEN', metavar="USER[:PASS] | TOKEN",
short_help='Credentials for the selected (-A) authentication method.', short_help="Credentials for the selected (-A) authentication method.",
help=""" help="""
For username/password based authentication mechanisms (e.g For username/password based authentication mechanisms (e.g
basic auth or digest auth) if only the username is provided basic auth or digest auth) if only the username is provided
@ -694,42 +719,42 @@ authentication.add_argument(
""", """,
) )
authentication.add_argument( authentication.add_argument(
'--auth-type', "--auth-type",
'-A', "-A",
action='lazy_choices', action="lazy_choices",
default=None, default=None,
getter=plugin_manager.get_auth_plugin_mapping, getter=plugin_manager.get_auth_plugin_mapping,
sort=True, sort=True,
cache=False, cache=False,
short_help='The authentication mechanism to be used.', short_help="The authentication mechanism to be used.",
help_formatter=format_auth_help, help_formatter=format_auth_help,
) )
authentication.add_argument( authentication.add_argument(
'--ignore-netrc', "--ignore-netrc",
default=False, default=False,
action='store_true', action="store_true",
short_help='Ignore credentials from .netrc.' short_help="Ignore credentials from .netrc.",
) )
####################################################################### #######################################################################
# Network # Network
####################################################################### #######################################################################
network = options.add_group('Network') network = options.add_group("Network")
network.add_argument( network.add_argument(
'--offline', "--offline",
default=False, default=False,
action='store_true', action="store_true",
short_help='Build the request and print it but dont actually send it.' short_help="Build the request and print it but dont actually send it.",
) )
network.add_argument( network.add_argument(
'--proxy', "--proxy",
default=[], default=[],
action='append', action="append",
metavar='PROTOCOL:PROXY_URL', metavar="PROTOCOL:PROXY_URL",
type=KeyValueArgType(SEPARATOR_PROXY), type=KeyValueArgType(SEPARATOR_PROXY),
short_help='String mapping of protocol to the URL of the proxy.', short_help="String mapping of protocol to the URL of the proxy.",
help=""" help="""
String mapping protocol to the URL of the proxy String mapping protocol to the URL of the proxy
(e.g. http:http://foo.bar:3128). You can specify multiple proxies with (e.g. http:http://foo.bar:3128). You can specify multiple proxies with
@ -739,39 +764,39 @@ network.add_argument(
""", """,
) )
network.add_argument( network.add_argument(
'--follow', "--follow",
'-F', "-F",
default=False, default=False,
action='store_true', action="store_true",
short_help='Follow 30x Location redirects.' short_help="Follow 30x Location redirects.",
) )
network.add_argument( network.add_argument(
'--max-redirects', "--max-redirects",
type=int, type=int,
default=30, default=30,
short_help='The maximum number of redirects that should be followed (with --follow).', short_help="The maximum number of redirects that should be followed (with --follow).",
help=""" help="""
By default, requests have a limit of 30 redirects (works with --follow). By default, requests have a limit of 30 redirects (works with --follow).
""", """,
) )
network.add_argument( network.add_argument(
'--max-headers', "--max-headers",
type=int, type=int,
default=0, default=0,
short_help=( short_help=(
'The maximum number of response headers to be read before ' "The maximum number of response headers to be read before "
'giving up (default 0, i.e., no limit).' "giving up (default 0, i.e., no limit)."
) ),
) )
network.add_argument( network.add_argument(
'--timeout', "--timeout",
type=float, type=float,
default=0, default=0,
metavar='SECONDS', metavar="SECONDS",
short_help='The connection timeout of the request in seconds.', short_help="The connection timeout of the request in seconds.",
help=""" help="""
The connection timeout of the request in seconds. The connection timeout of the request in seconds.
The default value is 0, i.e., there is no timeout limit. The default value is 0, i.e., there is no timeout limit.
@ -783,10 +808,10 @@ network.add_argument(
""", """,
) )
network.add_argument( network.add_argument(
'--check-status', "--check-status",
default=False, default=False,
action='store_true', action="store_true",
short_help='Exit with an error status code if the server replies with an error.', short_help="Exit with an error status code if the server replies with an error.",
help=""" help="""
By default, HTTPie exits with 0 when no network or other fatal errors By default, HTTPie exits with 0 when no network or other fatal errors
occur. This flag instructs HTTPie to also check the HTTP status code and occur. This flag instructs HTTPie to also check the HTTP status code and
@ -800,30 +825,30 @@ network.add_argument(
""", """,
) )
network.add_argument( network.add_argument(
'--path-as-is', "--path-as-is",
default=False, default=False,
action='store_true', action="store_true",
short_help='Bypass dot segment (/../ or /./) URL squashing.' short_help="Bypass dot segment (/../ or /./) URL squashing.",
) )
network.add_argument( network.add_argument(
'--chunked', "--chunked",
default=False, default=False,
action='store_true', action="store_true",
short_help=( short_help=(
'Enable streaming via chunked transfer encoding. ' "Enable streaming via chunked transfer encoding. "
'The Transfer-Encoding header is set to chunked.' "The Transfer-Encoding header is set to chunked."
) ),
) )
####################################################################### #######################################################################
# SSL # SSL
####################################################################### #######################################################################
ssl = options.add_group('SSL') ssl = options.add_group("SSL")
ssl.add_argument( ssl.add_argument(
'--verify', "--verify",
default='yes', default="yes",
short_help='If "no", skip SSL verification. If a file path, use it as a CA bundle.', short_help='If "no", skip SSL verification. If a file path, use it as a CA bundle.',
help=""" help="""
Set to "no" (or "false") to skip checking the host's SSL certificate. Set to "no" (or "false") to skip checking the host's SSL certificate.
@ -833,10 +858,10 @@ ssl.add_argument(
""", """,
) )
ssl.add_argument( ssl.add_argument(
'--ssl', "--ssl",
dest='ssl_version', dest="ssl_version",
choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()), choices=sorted(AVAILABLE_SSL_VERSION_ARG_MAPPING.keys()),
short_help='The desired protocol version to used.', short_help="The desired protocol version to used.",
help=""" help="""
The desired protocol version to use. This will default to The desired protocol version to use. This will default to
SSL v2.3 which will negotiate the highest protocol that both SSL v2.3 which will negotiate the highest protocol that both
@ -852,8 +877,8 @@ CIPHERS_CURRENT_DEFAULTS = (
See `http --help` for the default ciphers list on you system. See `http --help` for the default ciphers list on you system.
""" """
if IS_MAN_PAGE else if IS_MAN_PAGE
f""" else f"""
By default, the following ciphers are used on your system: By default, the following ciphers are used on your system:
{DEFAULT_SSL_CIPHERS_STRING} {DEFAULT_SSL_CIPHERS_STRING}
@ -861,21 +886,21 @@ CIPHERS_CURRENT_DEFAULTS = (
""" """
) )
ssl.add_argument( ssl.add_argument(
'--ciphers', "--ciphers",
short_help='A string in the OpenSSL cipher list format.', short_help="A string in the OpenSSL cipher list format.",
help=f""" help=f"""
A string in the OpenSSL cipher list format. A string in the OpenSSL cipher list format.
{CIPHERS_CURRENT_DEFAULTS} {CIPHERS_CURRENT_DEFAULTS}
""" """,
) )
ssl.add_argument( ssl.add_argument(
'--cert', "--cert",
default=None, default=None,
type=readable_file_arg, type=readable_file_arg,
short_help='Specifies a local cert to use as the client-side SSL certificate.', short_help="Specifies a local cert to use as the client-side SSL certificate.",
help=""" help="""
You can specify a local cert to use as client side SSL certificate. You can specify a local cert to use as client side SSL certificate.
This file may either contain both private key and certificate or you may This file may either contain both private key and certificate or you may
@ -884,10 +909,10 @@ ssl.add_argument(
""", """,
) )
ssl.add_argument( ssl.add_argument(
'--cert-key', "--cert-key",
default=None, default=None,
type=readable_file_arg, type=readable_file_arg,
short_help='The private key to use with SSL. Only needed if --cert is given.', short_help="The private key to use with SSL. Only needed if --cert is given.",
help=""" help="""
The private key to use with SSL. Only needed if --cert is given and the The private key to use with SSL. Only needed if --cert is given and the
certificate file does not contain the private key. certificate file does not contain the private key.
@ -896,63 +921,63 @@ ssl.add_argument(
) )
ssl.add_argument( ssl.add_argument(
'--cert-key-pass', "--cert-key-pass",
default=None, default=None,
type=SSLCredentials, type=SSLCredentials,
short_help='The passphrase to be used to with the given private key.', short_help="The passphrase to be used to with the given private key.",
help=""" help="""
The passphrase to be used to with the given private key. Only needed if --cert-key The passphrase to be used to with the given private key. Only needed if --cert-key
is given and the key file requires a passphrase. is given and the key file requires a passphrase.
If not provided, youll be prompted interactively. If not provided, youll be prompted interactively.
""" """,
) )
####################################################################### #######################################################################
# Troubleshooting # Troubleshooting
####################################################################### #######################################################################
troubleshooting = options.add_group('Troubleshooting') troubleshooting = options.add_group("Troubleshooting")
troubleshooting.add_argument( troubleshooting.add_argument(
'--ignore-stdin', "--ignore-stdin",
'-I', "-I",
action='store_true', action="store_true",
default=False, default=False,
short_help='Do not attempt to read stdin' short_help="Do not attempt to read stdin",
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--help', "--help",
action='help', action="help",
default=Qualifiers.SUPPRESS, default=Qualifiers.SUPPRESS,
short_help='Show this help message and exit.', short_help="Show this help message and exit.",
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--manual', "--manual",
action='manual', action="manual",
default=Qualifiers.SUPPRESS, default=Qualifiers.SUPPRESS,
short_help='Show the full manual.', short_help="Show the full manual.",
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--version', "--version",
action='version', action="version",
version=__version__, version=__version__,
short_help='Show version and exit.', short_help="Show version and exit.",
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--traceback', "--traceback",
action='store_true', action="store_true",
default=False, default=False,
short_help='Prints the exception traceback should one occur.', short_help="Prints the exception traceback should one occur.",
) )
troubleshooting.add_argument( troubleshooting.add_argument(
'--default-scheme', "--default-scheme",
default='http', default="http",
short_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( troubleshooting.add_argument(
'--debug', "--debug",
action='store_true', action="store_true",
default=False, default=False,
short_help='Print useful diagnostic information for bug reports.', short_help="Print useful diagnostic information for bug reports.",
help=""" help="""
Prints the exception traceback should one occur, as well as other Prints the exception traceback should one occur, as well as other
information useful for debugging HTTPie itself and for reporting bugs. information useful for debugging HTTPie itself and for reporting bugs.

View File

@ -17,13 +17,20 @@ from .cli.nested_json import NestedJSONSyntaxError
from .client import collect_messages from .client import collect_messages
from .context import Environment, LogLevel from .context import Environment, LogLevel
from .downloads import Downloader from .downloads import Downloader
from .http_parser import * from .http_parser import (
from .models import ( parse_single_request,
RequestsMessageKind, replace_global,
OutputOptions split_requests,
get_dependencies,
) )
from .models import RequestsMessageKind, OutputOptions
from .output.models import ProcessingOptions from .output.models import ProcessingOptions
from .output.writer import write_message, write_stream, write_raw_data, MESSAGE_SEPARATOR_BYTES from .output.writer import (
write_message,
write_stream,
write_raw_data,
MESSAGE_SEPARATOR_BYTES,
)
from .plugins.registry import plugin_manager from .plugins.registry import plugin_manager
from .status import ExitStatus, http_status_to_exit_status from .status import ExitStatus, http_status_to_exit_status
from .utils import unwrap_context from .utils import unwrap_context
@ -31,6 +38,7 @@ from .internal.update_warnings import check_updates
from .internal.daemon_runner import is_daemon_mode, run_daemon_task from .internal.daemon_runner import is_daemon_mode, run_daemon_task
from pathlib import Path from pathlib import Path
# noinspection PyDefaultArgument # noinspection PyDefaultArgument
def raw_main( def raw_main(
parser: argparse.ArgumentParser, parser: argparse.ArgumentParser,
@ -51,27 +59,27 @@ def raw_main(
if use_default_options and env.config.default_options: if use_default_options and env.config.default_options:
args = env.config.default_options + args args = env.config.default_options + args
include_debug_info = '--debug' in args include_debug_info = "--debug" in args
include_traceback = include_debug_info or '--traceback' in args include_traceback = include_debug_info or "--traceback" in args
def handle_generic_error(e, annotation=None): def handle_generic_error(e, annotation=None):
msg = str(e) msg = str(e)
if hasattr(e, 'request'): if hasattr(e, "request"):
request = e.request request = e.request
if hasattr(request, 'url'): if hasattr(request, "url"):
msg = ( msg = (
f'{msg} while doing a {request.method}' f"{msg} while doing a {request.method}"
f' request to URL: {request.url}' f" request to URL: {request.url}"
) )
if annotation: if annotation:
msg += annotation msg += annotation
env.log_error(f'{type(e).__name__}: {msg}') env.log_error(f"{type(e).__name__}: {msg}")
if include_traceback: if include_traceback:
raise raise
if include_debug_info: if include_debug_info:
print_debug_info(env) print_debug_info(env)
if args == ['--debug']: if args == ["--debug"]:
return ExitStatus.SUCCESS return ExitStatus.SUCCESS
exit_status = ExitStatus.SUCCESS exit_status = ExitStatus.SUCCESS
@ -87,13 +95,13 @@ def raw_main(
raise raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
except KeyboardInterrupt: except KeyboardInterrupt:
env.stderr.write('\n') env.stderr.write("\n")
if include_traceback: if include_traceback:
raise raise
exit_status = ExitStatus.ERROR_CTRL_C exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e: except SystemExit as e:
if e.code != ExitStatus.SUCCESS: if e.code != ExitStatus.SUCCESS:
env.stderr.write('\n') env.stderr.write("\n")
if include_traceback: if include_traceback:
raise raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
@ -105,33 +113,32 @@ def raw_main(
env=env, env=env,
) )
except KeyboardInterrupt: except KeyboardInterrupt:
env.stderr.write('\n') env.stderr.write("\n")
if include_traceback: if include_traceback:
raise raise
exit_status = ExitStatus.ERROR_CTRL_C exit_status = ExitStatus.ERROR_CTRL_C
except SystemExit as e: except SystemExit as e:
if e.code != ExitStatus.SUCCESS: if e.code != ExitStatus.SUCCESS:
env.stderr.write('\n') env.stderr.write("\n")
if include_traceback: if include_traceback:
raise raise
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
except requests.Timeout: except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT exit_status = ExitStatus.ERROR_TIMEOUT
env.log_error(f'Request timed out ({parsed_args.timeout}s).') env.log_error(f"Request timed out ({parsed_args.timeout}s).")
except requests.TooManyRedirects: except requests.TooManyRedirects:
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
env.log_error( env.log_error(
f'Too many redirects' f"Too many redirects (--max-redirects={parsed_args.max_redirects})."
f' (--max-redirects={parsed_args.max_redirects}).'
) )
except requests.exceptions.ConnectionError as exc: except requests.exceptions.ConnectionError as exc:
annotation = None annotation = None
original_exc = unwrap_context(exc) original_exc = unwrap_context(exc)
if isinstance(original_exc, socket.gaierror): if isinstance(original_exc, socket.gaierror):
if original_exc.errno == socket.EAI_AGAIN: if original_exc.errno == socket.EAI_AGAIN:
annotation = '\nCouldnt connect to a DNS server. Please check your connection and try again.' annotation = "\nCouldnt connect to a DNS server. Please check your connection and try again."
elif original_exc.errno == socket.EAI_NONAME: elif original_exc.errno == socket.EAI_NONAME:
annotation = '\nCouldnt resolve the given hostname. Please check the URL and try again.' annotation = "\nCouldnt resolve the given hostname. Please check the URL and try again."
propagated_exc = original_exc propagated_exc = original_exc
else: else:
propagated_exc = exc propagated_exc = exc
@ -147,8 +154,7 @@ def raw_main(
def main( def main(
args: List[Union[str, bytes]] = sys.argv, args: List[Union[str, bytes]] = sys.argv, env: Environment = Environment()
env: Environment = Environment()
) -> ExitStatus: ) -> ExitStatus:
""" """
The main function. The main function.
@ -162,12 +168,7 @@ def main(
from .cli.definition import parser from .cli.definition import parser
return raw_main( return raw_main(parser=parser, main_program=program, args=args, env=env)
parser=parser,
main_program=program,
args=args,
env=env
)
def program(args: argparse.Namespace, env: Environment) -> ExitStatus: def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
@ -185,7 +186,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
processing_options = ProcessingOptions.from_raw_args(args) processing_options = ProcessingOptions.from_raw_args(args)
def separate(): def separate():
getattr(env.stdout, 'buffer', env.stdout).write(MESSAGE_SEPARATOR_BYTES) getattr(env.stdout, "buffer", env.stdout).write(MESSAGE_SEPARATOR_BYTES)
def request_body_read_callback(chunk: bytes): def request_body_read_callback(chunk: bytes):
should_pipe_to_stdout = bool( should_pipe_to_stdout = bool(
@ -201,27 +202,35 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
env, env,
chunk, chunk,
processing_options=processing_options, processing_options=processing_options,
headers=initial_request.headers headers=initial_request.headers,
) )
try: try:
if args.download: if args.download:
args.follow = True # --download implies --follow. args.follow = True # --download implies --follow.
downloader = Downloader(env, output_file=args.output_file, resume=args.download_resume) downloader = Downloader(
env, output_file=args.output_file, resume=args.download_resume
)
downloader.pre_request(args.headers) downloader.pre_request(args.headers)
messages = collect_messages(
messages = collect_messages(env, args=args, env, args=args, request_body_read_callback=request_body_read_callback
request_body_read_callback=request_body_read_callback) )
force_separator = False force_separator = False
prev_with_body = False prev_with_body = False
# Process messages as theyre generated # Process messages as theyre generated
for message in messages: for message in messages:
output_options = OutputOptions.from_message(message, args.output_options) output_options = OutputOptions.from_message(
message, args.output_options
)
do_write_body = output_options.body do_write_body = output_options.body
if prev_with_body and output_options.any() and (force_separator or not env.stdout_isatty): if (
prev_with_body
and output_options.any()
and (force_separator or not env.stdout_isatty)
):
# Separate after a previous message with body, if needed. See test_tokens.py. # Separate after a previous message with body, if needed. See test_tokens.py.
separate() separate()
force_separator = False force_separator = False
@ -235,16 +244,21 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
else: else:
final_response = message final_response = message
if args.check_status or downloader: if args.check_status or downloader:
exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow) exit_status = http_status_to_exit_status(
if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1): http_status=message.status_code, follow=args.follow
env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING) )
if exit_status != ExitStatus.SUCCESS and (
not env.stdout_isatty or args.quiet == 1
):
env.log_error(
f"HTTP {message.raw.status} {message.raw.reason}",
level=LogLevel.WARNING,
)
write_message( write_message(
requests_message=message, requests_message=message,
env=env, env=env,
output_options=output_options._replace( output_options=output_options._replace(body=do_write_body),
body=do_write_body processing_options=processing_options,
),
processing_options=processing_options
) )
prev_with_body = output_options.body prev_with_body = output_options.body
@ -262,8 +276,8 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
if downloader.interrupted: if downloader.interrupted:
exit_status = ExitStatus.ERROR exit_status = ExitStatus.ERROR
env.log_error( env.log_error(
f'Incomplete download: size={downloader.status.total_size};' f"Incomplete download: size={downloader.status.total_size};"
f' downloaded={downloader.status.downloaded}' f" downloaded={downloader.status.downloaded}"
) )
return exit_status return exit_status
@ -272,16 +286,15 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
downloader.failed() downloader.failed()
if args.output_file and args.output_file_specified: if args.output_file and args.output_file_specified:
args.output_file.close() args.output_file.close()
if args.http_file: if args.http_file:
http_file = Path(args.url) http_file = Path(args.url)
if not http_file.exists(): if not http_file.exists():
raise FileNotFoundError(f"File not found: {args.url}") raise FileNotFoundError(f"File not found: {args.url}")
if not http_file.is_file(): if not http_file.is_file():
raise IsADirectoryError(f"Path is not a file: {args.url}") raise IsADirectoryError(f"Path is not a file: {args.url}")
http_contents = http_file.read_text() http_contents = http_file.read_text()
raw_requests = split_requests(replace_global(http_contents)) raw_requests = split_requests(replace_global(http_contents))
raw_requests = [req.strip() for req in raw_requests if req.strip()] raw_requests = [req.strip() for req in raw_requests if req.strip()]
parsed_requests = [] parsed_requests = []
@ -290,17 +303,19 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
for raw_req in raw_requests: for raw_req in raw_requests:
new_req = parse_single_request(raw_req) new_req = parse_single_request(raw_req)
if new_req is None:
continue
new_req.dependencies = get_dependencies(raw_req, req_names) new_req.dependencies = get_dependencies(raw_req, req_names)
if new_req.name is not None: if new_req.name is not None:
req_names.append(new_req.name) req_names.append(new_req.name)
else: else:
letters = string.ascii_letters + string.digits letters = string.ascii_letters + string.digits
new_req.name = ''.join(random.choice(letters) for _ in range(16)) new_req.name = "".join(random.choice(letters) for _ in range(16))
parsed_requests.append(new_req) parsed_requests.append(new_req)
args.url = new_req.url args.url = new_req.url
args.method = new_req.method args.method = new_req.method
args.headers = new_req.headers args.headers = new_req.headers
args.body = new_req.body args.data = new_req.body
response = actual_program(args, env) response = actual_program(args, env)
if new_req.name is not None: if new_req.name is not None:
@ -308,36 +323,31 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus:
all_success = all(r is ExitStatus.SUCCESS for r in responses.values()) all_success = all(r is ExitStatus.SUCCESS for r in responses.values())
return ExitStatus.SUCCESS if all_success else ExitStatus.ERROR return ExitStatus.SUCCESS if all_success else ExitStatus.ERROR
return actual_program(args, env) return actual_program(args, env)
def print_debug_info(env: Environment): def print_debug_info(env: Environment):
env.stderr.writelines([ env.stderr.writelines(
f'HTTPie {httpie_version}\n', [
f'Requests {requests_version}\n', f"HTTPie {httpie_version}\n",
f'Pygments {pygments_version}\n', f"Requests {requests_version}\n",
f'Python {sys.version}\n{sys.executable}\n', f"Pygments {pygments_version}\n",
f'{platform.system()} {platform.release()}', f"Python {sys.version}\n{sys.executable}\n",
]) f"{platform.system()} {platform.release()}",
env.stderr.write('\n\n') ]
)
env.stderr.write("\n\n")
env.stderr.write(repr(env)) env.stderr.write(repr(env))
env.stderr.write('\n\n') env.stderr.write("\n\n")
env.stderr.write(repr(plugin_manager)) env.stderr.write(repr(plugin_manager))
env.stderr.write('\n') env.stderr.write("\n")
def decode_raw_args( def decode_raw_args(args: List[Union[str, bytes]], stdin_encoding: str) -> List[str]:
args: List[Union[str, bytes]],
stdin_encoding: str
) -> List[str]:
""" """
Convert all bytes args to str Convert all bytes args to str
by decoding them using stdin encoding. by decoding them using stdin encoding.
""" """
return [ return [arg.decode(stdin_encoding) if type(arg) is bytes else arg for arg in args]
arg.decode(stdin_encoding)
if type(arg) is bytes else arg
for arg in args
]

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import re import re
@dataclass @dataclass
class HttpFileRequest: class HttpFileRequest:
method: str method: str
@ -14,31 +15,34 @@ class HttpFileRequest:
def split_requests(http_file_contents: str) -> list[str]: def split_requests(http_file_contents: str) -> list[str]:
"""Splits an HTTP file into individual requests but keeps the '###' in each request.""" """Splits an HTTP file into individual requests but keeps the '###' in each request."""
parts = re.split(r"(^###.*)", http_file_contents, flags=re.MULTILINE) parts = re.split(r"(^###.*)", http_file_contents, flags=re.MULTILINE)
requests = []
for i in range(1, len(parts), 2): requests = []
header = parts[i].strip() for part in parts:
body = parts[i + 1].strip() if i + 1 < len(parts) else "" if part.startswith("###"):
requests.append(f"{header}\n{body}") continue
part = part.strip()
if part:
requests.append(part)
return requests return requests
def get_dependencies(raw_http_request:str, poss_names: list[str]) -> list[str] | None: def get_dependencies(raw_http_request: str, poss_names: list[str]) -> list[str] | None:
"""returns a list of all the names of the requests that must be fufilled before this one can be sent""" """returns a list of all the names of the requests that must be fufilled before this one can be sent"""
pattern = r"\{\{(.*?)\}\}" pattern = r"\{\{(.*?)\}\}"
matches = re.findall(pattern, raw_http_request) matches = re.findall(pattern, raw_http_request)
if len(matches) == 0: if len(matches) == 0:
return None return None
names = [re.findall(r"^([A-Za-z0-9_]+).", match, re.MULTILINE) for match in matches] names = [re.findall(r"^([A-Za-z0-9_]+).", match, re.MULTILINE) for match in matches]
flat_names = [match for sublist in names for match in sublist] flat_names = [match for sublist in names for match in sublist]
if not all(name in poss_names for name in flat_names): if not all(name in poss_names for name in flat_names):
# TODO error not all dependencies exist # TODO error not all dependencies exist
return None return None
return flat_names return flat_names
def get_name(raw_http_request:str) -> str | None:
def get_name(raw_http_request: str) -> str | None:
"""returns the name of the http request if it has one, None otherwise""" """returns the name of the http request if it has one, None otherwise"""
matches = re.findall(r"^((//)|(#)) @name (.+)", raw_http_request, re.MULTILINE) matches = re.findall(r"^((//)|(#)) @name (.+)", raw_http_request, re.MULTILINE)
if len(matches) == 0: if len(matches) == 0:
@ -49,55 +53,64 @@ def get_name(raw_http_request:str) -> str | None:
# TODO error too many names # TODO error too many names
return None return None
def replace_global(http_file_contents_raw:str) -> str:
def replace_global(http_file_contents_raw: str) -> str:
"""finds and replaces all global variables by their values""" """finds and replaces all global variables by their values"""
# possible error when @variable=value is in the body # possible error when @variable=value is in the body
matches = re.findall(r"^@([A-Za-z0-9_]+)=(.+)$", http_file_contents_raw, re.MULTILINE) matches = re.findall(
r"^@([A-Za-z0-9_]+)=(.+)$", http_file_contents_raw, re.MULTILINE
)
http_file_contents_cooking = http_file_contents_raw http_file_contents_cooking = http_file_contents_raw
for variableName, value in matches: for variableName, value in matches:
http_file_contents_cooking = re.sub(rf"{{{{({re.escape(variableName)})}}}}",value , http_file_contents_cooking) http_file_contents_cooking = re.sub(
rf"{{{{({re.escape(variableName)})}}}}", value, http_file_contents_cooking
)
return http_file_contents_cooking return http_file_contents_cooking
def extract_headers(raw_text: list[str]) -> dict :
''' def extract_headers(raw_text: list[str]) -> dict:
"""
Extract the headers of the .http file Extract the headers of the .http file
Args: Args:
raw_text: the lines of the .http file containing the headers raw_text: the lines of the .http file containing the headers
Returns: Returns:
dict: containing the parsed headers dict: containing the parsed headers
''' """
headers = {} headers = {}
for line in raw_text: for line in raw_text:
if not line.strip() or ':' not in line: if not line.strip() or ":" not in line:
continue continue
header_name, header_value = line.split(':', 1) header_name, header_value = line.split(":", 1)
headers[header_name.strip()] = header_value.strip() headers[header_name.strip()] = header_value.strip()
return headers return headers
def parse_body(raw_text: str) -> bytes :
'''
parse the body of the .http file
'''
return b""
def parse_single_request(raw_text: str) -> HttpFileRequest: def parse_body(raw_text: str) -> bytes:
'''Parse a single request from .http file format to HttpFileRequest ''' """
parse the body of the .http file
"""
return raw_text
def parse_single_request(raw_text: str) -> HttpFileRequest | None:
"""Parse a single request from .http file format to HttpFileRequest"""
lines = raw_text.strip().splitlines() lines = raw_text.strip().splitlines()
lines = [line.strip() for line in lines if not line.strip().startswith("#")] lines = [line.strip() for line in lines if not line.strip().startswith("#")]
if not lines:
return None
method, url = lines[0].split(" ") method, url = lines[0].split(" ")
raw_headers = [] raw_headers = []
raw_body = [] raw_body = []
is_body = False is_body = False
for line in lines[1:]: for line in lines[1:]:
if not line.strip(): if not line.strip():
is_body = True is_body = True
@ -106,12 +119,12 @@ def parse_single_request(raw_text: str) -> HttpFileRequest:
raw_headers.append(line) raw_headers.append(line)
else: else:
raw_body.append(line) raw_body.append(line)
return HttpFileRequest( return HttpFileRequest(
method=method, method=method,
url=url, url=url,
headers=extract_headers(raw_headers), headers=extract_headers(raw_headers),
body=parse_body("\n".join(raw_body)), body=parse_body("\n".join(raw_body)),
dependencies={}, dependencies={},
name=get_name(raw_text) name=get_name(raw_text),
) )