Compare commits

...

24 Commits

Author SHA1 Message Date
af5e9897c9 Bump peter-evans/create-pull-request from 4 to 5
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4 to 5.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v4...v5)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-05 14:58:07 +00:00
47e9b99ba1 Bump actions/stale from 7 to 8 (#1492)
Bumps [actions/stale](https://github.com/actions/stale) from 7 to 8.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-28 18:30:30 -07:00
Sid
265841f866 docs: improve clarity of sentences (#1489)
* docs: improve clarity of sentences

Improved clarity by rephrasing sentences in the best practices section.

* docs: improve best practices section

* use appropriate formatting for stdin
* include EOF in abbreviated form

* docs: clarify sentence

* change 'know that' -> 'note that'
* use neither nor for better clarity
2023-03-22 13:03:18 -07:00
b16392fbb9 Remove redundant imports (#1466) 2023-01-15 11:35:36 -08:00
e73c3e6c24 Fix failing tests with responses ≥ 0.22.0
Close #1461
Close #1467

Thanks, @alexshpilkin!
2023-01-15 17:43:17 +01:00
f0563deb7f Bump mislav/bump-homebrew-formula-action from 1 to 2 (#1453)
Bumps [mislav/bump-homebrew-formula-action](https://github.com/mislav/bump-homebrew-formula-action) from 1 to 2.
- [Release notes](https://github.com/mislav/bump-homebrew-formula-action/releases)
- [Commits](https://github.com/mislav/bump-homebrew-formula-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: mislav/bump-homebrew-formula-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 03:18:15 -08:00
4894b4c0fc Bump actions/stale from 6 to 7 (#1459)
Bumps [actions/stale](https://github.com/actions/stale) from 6 to 7.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 03:17:54 -08:00
3a123c4125 Fix ci status badge error (#1464) 2023-01-04 03:17:18 -08:00
621042a048 Update README.md 2022-10-01 04:00:56 -07:00
0689b55e1d Clean up and refactor nested JSON parsing & interpreting (#1440) 2022-10-01 03:38:19 -07:00
a7321d8ac4 🔥 Remove $ from code fenced examples on readme (#1435)
* 🔥 Remove $ from code fenced examples on readme

* 🚨 FIx markdownlint errors

README.md:8: MD009 Trailing spaces
README.md:10: MD009 Trailing spaces
2022-10-01 03:37:50 -07:00
d9a73cd8eb Fix typos (#1431)
Found via `codespell -L datas`.
2022-10-01 03:34:41 -07:00
930cd9081a Use grep -E instead of egrep (#1436) 2022-10-01 03:32:17 -07:00
3549ee8342 Bump actions/stale from 5 to 6 (#1437)
Bumps [actions/stale](https://github.com/actions/stale) from 5 to 6.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-01 03:31:54 -07:00
810bb1c77b Update README.md 2022-08-10 09:13:49 -07:00
767f3c3a19 Update README.md 2022-08-10 07:58:09 -07:00
1236793272 Update README.md 2022-08-10 07:48:57 -07:00
c3a2f87dd2 Update README.md 2022-08-10 07:41:13 -07:00
1121d695a8 Update README.md 2022-08-10 07:38:40 -07:00
5794a070e1 place the logo in the middle in README.md (#1393) 2022-07-12 08:33:57 -07:00
4736a16698 docs: Fix a few typos (#1419) 2022-07-03 14:54:34 +02:00
3ad408add7 Fix paths to run benchmarking script (#1416) 2022-06-19 00:20:22 -07:00
91cdb22a4b Update Requests documentation links (#1414) 2022-06-17 14:04:42 -07:00
c995fd9b24 Bump actions/setup-python from 3 to 4 (#1412)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 3 to 4.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-09 10:04:06 -07:00
35 changed files with 602 additions and 540 deletions

View File

@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9
@ -20,7 +20,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: "[automated] Update auto-generated files" commit-message: "[automated] Update auto-generated files"
title: "[automated] Update auto-generated files" title: "[automated] Update auto-generated files"

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: "3.9" python-version: "3.9"

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9
- run: make venv - run: make venv

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
- run: make install - run: make install

View File

@ -18,7 +18,7 @@ jobs:
with: with:
ref: ${{ github.event.inputs.branch }} ref: ${{ github.event.inputs.branch }}
- uses: mislav/bump-homebrew-formula-action@v1 - uses: mislav/bump-homebrew-formula-action@v2
with: with:
formula-name: httpie formula-name: httpie
tag-name: ${{ github.events.inputs.branch }} tag-name: ${{ github.events.inputs.branch }}

View File

@ -24,7 +24,7 @@ jobs:
with: with:
ref: ${{ github.event.inputs.branch }} ref: ${{ github.event.inputs.branch }}
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9

View File

@ -17,7 +17,7 @@ jobs:
with: with:
ref: ${{ github.event.inputs.branch }} ref: ${{ github.event.inputs.branch }}
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v5 - uses: actions/stale@v8
with: with:
close-pr-message: 'Thanks for the pull request, but since it was stale for more than a 30 days we are closing it. If you want to work back on it, feel free to re-open it or create a new one.' close-pr-message: 'Thanks for the pull request, but since it was stale for more than a 30 days we are closing it. If you want to work back on it, feel free to re-open it or create a new one.'
stale-pr-label: 'stale' stale-pr-label: 'stale'

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v3 - uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Windows setup - name: Windows setup

View File

@ -154,7 +154,7 @@ with the master branch of your repository (or a fresh checkout of HTTPie master,
`--fresh`) and report the results back. `--fresh`) and report the results back.
```bash ```bash
$ python extras/benchmarks/run.py $ python extras/profiling/run.py
``` ```
The benchmarks can also be run on the CI. Since it is a long process, it requires manual The benchmarks can also be run on the CI. Since it is a long process, it requires manual

View File

@ -34,7 +34,7 @@ default: list-tasks
list-tasks: list-tasks:
@echo Available tasks: @echo Available tasks:
@echo ---------------- @echo ----------------
@$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | grep -E -v -e '^[^[:alnum:]]' -e '^$@$$'
@echo @echo

View File

@ -1,10 +1,30 @@
<br/> <h2 align="center">
<a href="https://httpie.io" target="blank_"> <a href="https://httpie.io" target="blank_">
<img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" /> <img height="100" alt="HTTPie" src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-logo.svg" />
</a> </a>
<br/> <br>
HTTPie for Terminal: human-friendly CLI HTTP client for the API era
</h2>
# HTTPie: human-friendly CLI HTTP client for the API era <div align="center">
[![HTTPie for Desktop](https://img.shields.io/static/v1?label=HTTPie&message=for%20Desktop&color=4B78E6)](https://httpie.io/product)
[![](https://img.shields.io/static/v1?label=HTTPie&message=for%20Web%20%26%20Mobile&color=73DC8C)](https://httpie.io/app)
[![](https://img.shields.io/static/v1?label=HTTPie&message=for%20Terminal&color=FA9BFA)](https://httpie.io/cli)
[![Twitter](https://img.shields.io/twitter/follow/httpie?style=flat&color=%234B78E6&logoColor=%234B78E6)](https://twitter.com/httpie)
[![Chat](https://img.shields.io/discord/725351238698270761?style=flat&label=Chat%20on%20Discord&color=%23FA9BFA)](https://httpie.io/discord)
</div>
<div align="center">
[![Docs](https://img.shields.io/badge/stable%20docs-httpie.io%2Fdocs%2Fcli-brightgreen?style=flat&color=%2373DC8C&label=Docs)](https://httpie.org/docs/cli)
[![Latest version](https://img.shields.io/pypi/v/httpie.svg?style=flat&label=Latest&color=%234B78E6&logo=&logoColor=white)](https://pypi.python.org/pypi/httpie)
[![Build](https://img.shields.io/github/actions/workflow/status/httpie/httpie/tests.yml?branch=master&color=%23FA9BFA&label=Build)](https://github.com/httpie/httpie/actions)
[![Coverage](https://img.shields.io/codecov/c/github/httpie/httpie?style=flat&label=Coverage&color=%2373DC8C)](https://codecov.io/gh/httpie/httpie)
</div>
HTTPie (pronounced _aitch-tee-tee-pie_) is a command-line HTTP client. HTTPie (pronounced _aitch-tee-tee-pie_) is a command-line HTTP client.
Its goal is to make CLI interaction with web services as human-friendly as possible. Its goal is to make CLI interaction with web services as human-friendly as possible.
@ -12,16 +32,16 @@ HTTPie is designed for testing, debugging, and generally interacting with APIs &
The `http` & `https` commands allow for creating and sending arbitrary HTTP requests. The `http` & `https` commands allow for creating and sending arbitrary HTTP requests.
They use simple and natural syntax and provide formatted and colorized output. They use simple and natural syntax and provide formatted and colorized output.
[![Docs](https://img.shields.io/badge/stable%20docs-httpie.io%2Fdocs-brightgreen?style=flat&color=%2373DC8C&label=Docs)](https://httpie.org/docs) <div align="center">
[![Latest version](https://img.shields.io/pypi/v/httpie.svg?style=flat&label=Latest&color=%234B78E6&logo=&logoColor=white)](https://pypi.python.org/pypi/httpie)
[![Build](https://img.shields.io/github/workflow/status/httpie/httpie/Build?color=%23FA9BFA&label=Build)](https://github.com/httpie/httpie/actions)
[![Coverage](https://img.shields.io/codecov/c/github/httpie/httpie?style=flat&label=Coverage&color=%2373DC8C)](https://codecov.io/gh/httpie/httpie)
[![Twitter](https://img.shields.io/twitter/follow/httpie?style=flat&color=%234B78E6&logoColor=%234B78E6)](https://twitter.com/httpie)
[![Chat](https://img.shields.io/badge/chat-Discord-brightgreen?style=flat&label=Chat%20on&color=%23FA9BFA)](https://httpie.io/discord)
<img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/> <img src="https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif" alt="HTTPie in action" width="100%"/>
</div>
## We lost 54k GitHub stars ## We lost 54k GitHub stars
Please note we recently accidentally made this repo private for a moment, and GitHub deleted our community that took a decade to build. Read the full story here: https://httpie.io/blog/stardust Please note we recently accidentally made this repo private for a moment, and GitHub deleted our community that took a decade to build. Read the full story here: https://httpie.io/blog/stardust
@ -53,25 +73,25 @@ Please note we recently accidentally made this repo private for a moment, and Gi
Hello World: Hello World:
```bash ```bash
$ https httpie.io/hello https httpie.io/hello
``` ```
Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data: Custom [HTTP method](https://httpie.io/docs#http-method), [HTTP headers](https://httpie.io/docs#http-headers) and [JSON](https://httpie.io/docs#json) data:
```bash ```bash
$ http PUT pie.dev/put X-API-Token:123 name=John http PUT pie.dev/put X-API-Token:123 name=John
``` ```
Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode): Build and print a request without sending it using [offline mode](https://httpie.io/docs#offline-mode):
```bash ```bash
$ http --offline pie.dev/post hello=offline http --offline pie.dev/post hello=offline
``` ```
Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication): Use [GitHub API](https://developer.github.com/v3/issues/comments/#create-a-comment) to post a comment on an [Issue](https://github.com/httpie/httpie/issues/83) with [authentication](https://httpie.io/docs#authentication):
```bash ```bash
$ http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:' http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'
``` ```
[See more examples →](https://httpie.io/docs#examples) [See more examples →](https://httpie.io/docs#examples)

View File

@ -1453,7 +1453,8 @@ $ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
## HTTPS ## HTTPS
### Server SSL certificate verification ### Server SSL certificate verification
To skip the hosts SSL certificate verification, you can pass `--verify=no` (default is `yes`):
To skip the hosts SSL certificate verification, you can pass `--verify=no` (default is `yes`):
```bash ```bash
$ http --verify=no https://pie.dev/get $ http --verify=no https://pie.dev/get
@ -2394,12 +2395,9 @@ fi
You can check whether a new update is available for your system by running `httpie cli check-updates`: You can check whether a new update is available for your system by running `httpie cli check-updates`:
```bash-termible ```bash-termible
You can check whether a new update is available for your system by running `httpie cli check-updates`: $ httpie cli check-updates
```bash-termible
$ httpie cli check-updates
``` ```
#### `httpie cli export-args` #### `httpie cli export-args`
`httpie cli export-args` command can expose the parser specification of `http`/`https` commands `httpie cli export-args` command can expose the parser specification of `http`/`https` commands
@ -2553,7 +2551,7 @@ HTTPie has the following community channels:
See [github.com/httpie/httpie/security/policy](https://github.com/httpie/httpie/security/policy). See [github.com/httpie/httpie/security/policy](https://github.com/httpie/httpie/security/policy).
### Change log ### Change log
See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md). See [CHANGELOG](https://github.com/httpie/httpie/blob/master/CHANGELOG.md).

View File

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

View File

@ -55,7 +55,7 @@ def build_docs_structure(database: Database):
tree = database[KEY_DOC_STRUCTURE] tree = database[KEY_DOC_STRUCTURE]
structure = [] structure = []
for platform, tools_ids in tree.items(): for platform, tools_ids in tree.items():
assert platform.isalnum(), f'{platform=} must be alpha-numeric for generated links to work' assert platform.isalnum(), f'{platform=} must be alphanumeric for generated links to work'
platform_tools = [tools[tool_id] for tool_id in tools_ids] platform_tools = [tools[tool_id] for tool_id in tools_ids]
structure.append((platform, platform_tools)) structure.append((platform, platform_tools))
return structure return structure

View File

@ -10,7 +10,7 @@ Ensure the following requirements are satisfied:
- Python 3.7+ - Python 3.7+
- `pyperf` - `pyperf`
Then, run the `extras/benchmarks/run.py`: Then, run the `extras/profiling/run.py`:
```console ```console
$ python extras/profiling/run.py $ python extras/profiling/run.py

View File

@ -9,11 +9,11 @@ timings.
The benchmarks are run through 'pyperf', which allows to The benchmarks are run through 'pyperf', which allows to
do get very precise results. For micro-benchmarks like startup, do get very precise results. For micro-benchmarks like startup,
please run `pyperf system tune` to get even more acurrate results. please run `pyperf system tune` to get even more accurate results.
Examples: Examples:
# Run everything as usual, the default is that we do 3 warmup runs # Run everything as usual, the default is that we do 3 warm-up runs
# and 5 actual runs. # and 5 actual runs.
$ python extras/profiling/benchmarks.py $ python extras/profiling/benchmarks.py
@ -188,7 +188,7 @@ DownloadRunner('download', '`http --download :/big_file.txt` (3GB)', '3G')
def main() -> None: def main() -> None:
# PyPerf will bring it's own argument parser, so configure the script. # PyPerf will bring it's own argument parser, so configure the script.
# The somewhat fast and also precise enough configuration is this. We run # The somewhat fast and also precise enough configuration is this. We run
# benchmarks 3 times to warmup (e.g especially for download benchmark, this # benchmarks 3 times to warm up (e.g especially for download benchmark, this
# is important). And then 5 actual runs where we record. # is important). And then 5 actual runs where we record.
sys.argv.extend( sys.argv.extend(
['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2'] ['--worker', '--loops=1', '--warmup=3', '--values=5', '--processes=2']

View File

@ -19,19 +19,19 @@ which would include additional dependencies like pyOpenSSL.
Examples: Examples:
# Run everything as usual, and compare last commit with master # Run everything as usual, and compare last commit with master
$ python extras/benchmarks/run.py $ python extras/profiling/run.py
# Include complex environments # Include complex environments
$ python extras/benchmarks/run.py --complex $ python extras/profiling/run.py --complex
# Compare against a fresh copy # Compare against a fresh copy
$ python extras/benchmarks/run.py --fresh $ python extras/profiling/run.py --fresh
# Compare against a custom branch of a custom repo # Compare against a custom branch of a custom repo
$ python extras/benchmarks/run.py --target-repo my_repo --target-branch my_branch $ python extras/profiling/run.py --target-repo my_repo --target-branch my_branch
# Debug changes made on this script (only run benchmarks once) # Debug changes made on this script (only run benchmarks once)
$ python extras/benchmarks/run.py --debug $ python extras/profiling/run.py --debug
""" """
import dataclasses import dataclasses

View File

@ -9,7 +9,7 @@ from httpie.cli.options import ParserSpec
from httpie.manager.cli import options as manager_options from httpie.manager.cli import options as manager_options
from httpie.output.ui.rich_help import OptionsHighlighter, to_usage from httpie.output.ui.rich_help import OptionsHighlighter, to_usage
from httpie.output.ui.rich_utils import render_as_string from httpie.output.ui.rich_utils import render_as_string
from httpie.utils import split from httpie.utils import split_iterable
# Escape certain characters so they are rendered properly on # Escape certain characters so they are rendered properly on

View File

@ -92,7 +92,3 @@ class MultipartRequestDataDict(MultiValueOrderedDict):
class RequestFilesDict(RequestDataDict): class RequestFilesDict(RequestDataDict):
pass pass
class NestedJSONArray(list):
"""Denotes a top-level JSON array."""

View File

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

View File

@ -0,0 +1,20 @@
"""
A library for parsing the HTTPie nested JSON key syntax and constructing the resulting objects.
<https://httpie.io/docs/cli/nested-json>
It has no dependencies.
"""
from .interpret import interpret_nested_json, unwrap_top_level_list_if_needed
from .errors import NestedJSONSyntaxError
from .tokens import EMPTY_STRING, NestedJSONArray
__all__ = [
'interpret_nested_json',
'unwrap_top_level_list_if_needed',
'EMPTY_STRING',
'NestedJSONArray',
'NestedJSONSyntaxError'
]

View File

@ -0,0 +1,27 @@
from typing import Optional
from .tokens import Token, HIGHLIGHTER
class NestedJSONSyntaxError(ValueError):
def __init__(
self,
source: str,
token: Optional[Token],
message: str,
message_kind: str = 'Syntax',
) -> None:
self.source = source
self.token = token
self.message = message
self.message_kind = message_kind
def __str__(self):
lines = [f'HTTPie {self.message_kind} Error: {self.message}']
if self.token is not None:
lines.append(self.source)
lines.append(
' ' * self.token.start
+ HIGHLIGHTER * (self.token.end - self.token.start)
)
return '\n'.join(lines)

View File

@ -0,0 +1,129 @@
from typing import Type, Union, Any, Iterable, Tuple
from .parse import parse, assert_cant_happen
from .errors import NestedJSONSyntaxError
from .tokens import EMPTY_STRING, TokenKind, Token, PathAction, Path, NestedJSONArray
__all__ = [
'interpret_nested_json',
'unwrap_top_level_list_if_needed',
]
JSONType = Type[Union[dict, list, int, float, str]]
JSON_TYPE_MAPPING = {
dict: 'object',
list: 'array',
int: 'number',
float: 'number',
str: 'string',
}
def interpret_nested_json(pairs: Iterable[Tuple[str, str]]) -> dict:
context = None
for key, value in pairs:
context = interpret(context, key, value)
return wrap_with_dict(context)
def interpret(context: Any, key: str, value: Any) -> Any:
cursor = context
paths = list(parse(key))
paths.append(Path(PathAction.SET, value))
# noinspection PyShadowingNames
def type_check(index: int, path: Path, expected_type: JSONType):
if not isinstance(cursor, expected_type):
if path.tokens:
pseudo_token = Token(
kind=TokenKind.PSEUDO,
value='',
start=path.tokens[0].start,
end=path.tokens[-1].end,
)
else:
pseudo_token = None
cursor_type = JSON_TYPE_MAPPING.get(type(cursor), type(cursor).__name__)
required_type = JSON_TYPE_MAPPING[expected_type]
message = f'Cannot perform {path.kind.to_string()!r} based access on '
message += repr(''.join(path.reconstruct() for path in paths[:index]))
message += f' which has a type of {cursor_type!r} but this operation'
message += f' requires a type of {required_type!r}.'
raise NestedJSONSyntaxError(
source=key,
token=pseudo_token,
message=message,
message_kind='Type',
)
def object_for(kind: PathAction) -> Any:
if kind is PathAction.KEY:
return {}
elif kind in {PathAction.INDEX, PathAction.APPEND}:
return []
else:
assert_cant_happen()
for index, (path, next_path) in enumerate(zip(paths, paths[1:])):
# If there is no context yet, set it.
if cursor is None:
context = cursor = object_for(path.kind)
if path.kind is PathAction.KEY:
type_check(index, path, dict)
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
cursor = cursor.setdefault(path.accessor, object_for(next_path.kind))
elif path.kind is PathAction.INDEX:
type_check(index, path, list)
if path.accessor < 0:
raise NestedJSONSyntaxError(
source=key,
token=path.tokens[1],
message='Negative indexes are not supported.',
message_kind='Value',
)
cursor.extend([None] * (path.accessor - len(cursor) + 1))
if next_path.kind is PathAction.SET:
cursor[path.accessor] = next_path.accessor
break
if cursor[path.accessor] is None:
cursor[path.accessor] = object_for(next_path.kind)
cursor = cursor[path.accessor]
elif path.kind is PathAction.APPEND:
type_check(index, path, list)
if next_path.kind is PathAction.SET:
cursor.append(next_path.accessor)
break
cursor.append(object_for(next_path.kind))
cursor = cursor[-1]
else:
assert_cant_happen()
return context
def wrap_with_dict(context):
if context is None:
return {}
elif isinstance(context, list):
return {
EMPTY_STRING: NestedJSONArray(context),
}
else:
assert isinstance(context, dict)
return context
def unwrap_top_level_list_if_needed(data: dict):
"""
Propagate the top-level list, if thats what we got.
"""
if len(data) == 1:
key, value = list(data.items())[0]
if isinstance(value, NestedJSONArray):
assert key == EMPTY_STRING
return value
return data

View File

@ -0,0 +1,193 @@
from typing import Iterator
from .errors import NestedJSONSyntaxError
from .tokens import (
EMPTY_STRING,
BACKSLASH,
TokenKind,
OPERATORS,
SPECIAL_CHARS,
LITERAL_TOKENS,
Token,
PathAction,
Path,
)
__all__ = [
'parse',
'assert_cant_happen',
]
def parse(source: str) -> Iterator[Path]:
"""
start: root_path path*
root_path: (literal | index_path | append_path)
literal: TEXT | NUMBER
path:
key_path
| index_path
| append_path
key_path: LEFT_BRACKET TEXT RIGHT_BRACKET
index_path: LEFT_BRACKET NUMBER RIGHT_BRACKET
append_path: LEFT_BRACKET RIGHT_BRACKET
"""
tokens = list(tokenize(source))
cursor = 0
def can_advance():
return cursor < len(tokens)
# noinspection PyShadowingNames
def expect(*kinds):
nonlocal cursor
assert kinds
if can_advance():
token = tokens[cursor]
cursor += 1
if token.kind in kinds:
return token
elif tokens:
token = tokens[-1]._replace(
start=tokens[-1].end + 0,
end=tokens[-1].end + 1,
)
else:
token = None
if len(kinds) == 1:
suffix = kinds[0].to_name()
else:
suffix = ', '.join(kind.to_name() for kind in kinds[:-1])
suffix += ' or ' + kinds[-1].to_name()
message = f'Expecting {suffix}'
raise NestedJSONSyntaxError(source, token, message)
# noinspection PyShadowingNames
def parse_root():
tokens = []
if not can_advance():
return Path(
kind=PathAction.KEY,
accessor=EMPTY_STRING,
is_root=True
)
# (literal | index_path | append_path)?
token = expect(*LITERAL_TOKENS, TokenKind.LEFT_BRACKET)
tokens.append(token)
if token.kind in LITERAL_TOKENS:
action = PathAction.KEY
value = str(token.value)
elif token.kind is TokenKind.LEFT_BRACKET:
token = expect(TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
tokens.append(token)
if token.kind is TokenKind.NUMBER:
action = PathAction.INDEX
value = token.value
tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.RIGHT_BRACKET:
action = PathAction.APPEND
value = None
else:
assert_cant_happen()
else:
assert_cant_happen()
# noinspection PyUnboundLocalVariable
return Path(
kind=action,
accessor=value,
tokens=tokens,
is_root=True
)
yield parse_root()
# path*
while can_advance():
path_tokens = [expect(TokenKind.LEFT_BRACKET)]
token = expect(TokenKind.TEXT, TokenKind.NUMBER, TokenKind.RIGHT_BRACKET)
path_tokens.append(token)
if token.kind is TokenKind.RIGHT_BRACKET:
path = Path(PathAction.APPEND, tokens=path_tokens)
elif token.kind is TokenKind.TEXT:
path = Path(PathAction.KEY, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
elif token.kind is TokenKind.NUMBER:
path = Path(PathAction.INDEX, token.value, tokens=path_tokens)
path_tokens.append(expect(TokenKind.RIGHT_BRACKET))
else:
assert_cant_happen()
# noinspection PyUnboundLocalVariable
yield path
def tokenize(source: str) -> Iterator[Token]:
cursor = 0
backslashes = 0
buffer = []
def send_buffer() -> Iterator[Token]:
nonlocal backslashes
if not buffer:
return None
value = ''.join(buffer)
kind = TokenKind.TEXT
if not backslashes:
for variation, kind in [
(int, TokenKind.NUMBER),
(check_escaped_int, TokenKind.TEXT),
]:
try:
value = variation(value)
except ValueError:
continue
else:
break
yield Token(
kind=kind,
value=value,
start=cursor - (len(buffer) + backslashes),
end=cursor,
)
buffer.clear()
backslashes = 0
def can_advance() -> bool:
return cursor < len(source)
while can_advance():
index = source[cursor]
if index in OPERATORS:
yield from send_buffer()
yield Token(OPERATORS[index], index, cursor, cursor + 1)
elif index == BACKSLASH and can_advance():
if source[cursor + 1] in SPECIAL_CHARS:
backslashes += 1
else:
buffer.append(index)
buffer.append(source[cursor + 1])
cursor += 1
else:
buffer.append(index)
cursor += 1
yield from send_buffer()
def check_escaped_int(value: str) -> str:
if not value.startswith(BACKSLASH):
raise ValueError('Not an escaped int')
try:
int(value[1:])
except ValueError as exc:
raise ValueError('Not an escaped int') from exc
else:
return value[1:]
def assert_cant_happen():
raise ValueError('Unexpected value')

View File

@ -0,0 +1,80 @@
from enum import Enum, auto
from typing import NamedTuple, Union, Optional, List
EMPTY_STRING = ''
HIGHLIGHTER = '^'
OPEN_BRACKET = '['
CLOSE_BRACKET = ']'
BACKSLASH = '\\'
class TokenKind(Enum):
TEXT = auto()
NUMBER = auto()
LEFT_BRACKET = auto()
RIGHT_BRACKET = auto()
PSEUDO = auto() # Not a real token, use when representing location only.
def to_name(self) -> str:
for key, value in OPERATORS.items():
if value is self:
return repr(key)
else:
return 'a ' + self.name.lower()
OPERATORS = {
OPEN_BRACKET: TokenKind.LEFT_BRACKET,
CLOSE_BRACKET: TokenKind.RIGHT_BRACKET,
}
SPECIAL_CHARS = OPERATORS.keys() | {BACKSLASH}
LITERAL_TOKENS = [
TokenKind.TEXT,
TokenKind.NUMBER,
]
class Token(NamedTuple):
kind: TokenKind
value: Union[str, int]
start: int
end: int
class PathAction(Enum):
KEY = auto()
INDEX = auto()
APPEND = auto()
# Pseudo action, used by the interpreter
SET = auto()
def to_string(self) -> str:
return self.name.lower()
class Path:
def __init__(
self,
kind: PathAction,
accessor: Optional[Union[str, int]] = None,
tokens: Optional[List[Token]] = None,
is_root: bool = False,
):
self.kind = kind
self.accessor = accessor
self.tokens = tokens or []
self.is_root = is_root
def reconstruct(self) -> str:
if self.kind is PathAction.KEY:
if self.is_root:
return str(self.accessor)
return OPEN_BRACKET + self.accessor + CLOSE_BRACKET
elif self.kind is PathAction.INDEX:
return OPEN_BRACKET + str(self.accessor) + CLOSE_BRACKET
elif self.kind is PathAction.APPEND:
return OPEN_BRACKET + CLOSE_BRACKET
class NestedJSONArray(list):
"""Denotes a top-level JSON array."""

View File

@ -18,7 +18,7 @@ from .dicts import (
) )
from .exceptions import ParseError from .exceptions import ParseError
from .nested_json import interpret_nested_json from .nested_json import interpret_nested_json
from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys, split_iterable
class RequestItems: class RequestItems:
@ -78,25 +78,28 @@ class RequestItems:
instance.data, instance.data,
), ),
SEPARATOR_DATA_RAW_JSON: ( SEPARATOR_DATA_RAW_JSON: (
json_only(instance, process_data_raw_json_embed_arg), convert_json_value_to_form_if_needed(
in_json_mode=instance.is_json,
processor=process_data_raw_json_embed_arg
),
instance.data, instance.data,
), ),
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: ( SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
json_only(instance, process_data_embed_raw_json_file_arg), convert_json_value_to_form_if_needed(
in_json_mode=instance.is_json,
processor=process_data_embed_raw_json_file_arg,
),
instance.data, instance.data,
), ),
} }
if instance.is_json: if instance.is_json:
json_item_args, request_item_args = split( json_item_args, request_item_args = split_iterable(
request_item_args, iterable=request_item_args,
lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS key=lambda arg: arg.sep in SEPARATOR_GROUP_NESTED_JSON_ITEMS
) )
if json_item_args: if json_item_args:
pairs = [ pairs = [(arg.key, rules[arg.sep][0](arg)) for arg in json_item_args]
(arg.key, rules[arg.sep][0](arg))
for arg in json_item_args
]
processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS] processor_func, target_dict = rules[SEPARATOR_GROUP_NESTED_JSON_ITEMS]
value = processor_func(pairs) value = processor_func(pairs)
target_dict.update(value) target_dict.update(value)
@ -159,6 +162,30 @@ def process_file_upload_arg(arg: KeyValueArg) -> Tuple[str, IO, str]:
) )
def convert_json_value_to_form_if_needed(in_json_mode: bool, processor: Callable[[KeyValueArg], JSONType]) -> Callable[[], str]:
"""
We allow primitive values to be passed to forms via JSON key/value syntax.
But complex values lead to an error because theres no clear way to serialize them.
"""
if in_json_mode:
return processor
@functools.wraps(processor)
def wrapper(*args, **kwargs) -> str:
try:
output = processor(*args, **kwargs)
except ParseError:
output = None
if isinstance(output, (str, int, float)):
return str(output)
else:
raise ParseError('Cannot use complex JSON value types with --form/--multipart.')
return wrapper
def process_data_item_arg(arg: KeyValueArg) -> str: def process_data_item_arg(arg: KeyValueArg) -> str:
return arg.value return arg.value
@ -167,29 +194,6 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
return load_text_file(arg) return load_text_file(arg)
def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
if items.is_json:
return func
@functools.wraps(func)
def wrapper(*args, **kwargs) -> str:
try:
ret = func(*args, **kwargs)
except ParseError:
ret = None
# If it is a basic type, then allow it
if isinstance(ret, (str, int, float)):
return str(ret)
else:
raise ParseError(
'Can\'t use complex JSON value types with '
'--form/--multipart.'
)
return wrapper
def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType: def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
contents = load_text_file(arg) contents = load_text_file(arg)
value = load_json(arg, contents) value = load_json(arg, contents)

View File

@ -10,12 +10,13 @@ from urllib.parse import urlparse, urlunparse
import requests import requests
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import urllib3 import urllib3
from . import __version__ from . import __version__
from .adapters import HTTPieHTTPAdapter from .adapters import HTTPieHTTPAdapter
from .context import Environment
from .cli.constants import HTTP_OPTIONS from .cli.constants import HTTP_OPTIONS
from .cli.nested_json import EMPTY_STRING from .cli.dicts import HTTPHeadersDict
from .cli.dicts import HTTPHeadersDict, NestedJSONArray from .cli.nested_json import unwrap_top_level_list_if_needed
from .context import Environment
from .encoding import UTF8 from .encoding import UTF8
from .models import RequestsMessage from .models import RequestsMessage
from .plugins.registry import plugin_manager from .plugins.registry import plugin_manager
@ -306,21 +307,13 @@ def make_send_kwargs_mergeable_from_env(args: argparse.Namespace) -> dict:
def json_dict_to_request_body(data: Dict[str, Any]) -> str: def json_dict_to_request_body(data: Dict[str, Any]) -> str:
# Propagate the top-level list if there is only one data = unwrap_top_level_list_if_needed(data)
# item in the object, with an en empty key.
if len(data) == 1:
[(key, value)] = data.items()
if isinstance(value, NestedJSONArray):
assert key == EMPTY_STRING
data = value
if data: if data:
data = json.dumps(data) data = json.dumps(data)
else: else:
# We need to set data to an empty string to prevent requests # We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`. # from assigning an empty list to `response.request.data`.
data = '' data = ''
return data return data

View File

@ -11,7 +11,7 @@ from requests import __version__ as requests_version
from . import __version__ as httpie_version from . import __version__ as httpie_version
from .cli.constants import OUT_REQ_BODY from .cli.constants import OUT_REQ_BODY
from .cli.nested_json import HTTPieSyntaxError 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
@ -78,7 +78,7 @@ def raw_main(
args=args, args=args,
env=env, env=env,
) )
except HTTPieSyntaxError as exc: except NestedJSONSyntaxError as exc:
env.stderr.write(str(exc) + "\n") env.stderr.write(str(exc) + "\n")
if include_traceback: if include_traceback:
raise raise

View File

@ -52,7 +52,6 @@ def program():
try: try:
exit_status = main() exit_status = main()
except KeyboardInterrupt: except KeyboardInterrupt:
from httpie.status import ExitStatus
exit_status = ExitStatus.ERROR_CTRL_C exit_status = ExitStatus.ERROR_CTRL_C
return exit_status return exit_status

View File

@ -43,7 +43,6 @@ def _discover_system_pip() -> List[str]:
def _run_pip_subprocess(pip_executable: List[str], args: List[str]) -> bytes: def _run_pip_subprocess(pip_executable: List[str], args: List[str]) -> bytes:
import subprocess
cmd = [*pip_executable, *args] cmd = [*pip_executable, *args]
try: try:

View File

@ -16,7 +16,6 @@ from .cli.constants import (
from .compat import cached_property from .compat import cached_property
from .utils import split_cookies, parse_content_type_header from .utils import split_cookies, parse_content_type_header
ELAPSED_TIME_LABEL = 'Elapsed time' ELAPSED_TIME_LABEL = 'Elapsed time'
@ -67,27 +66,10 @@ class HTTPResponse(HTTPMessage):
def iter_lines(self, chunk_size): def iter_lines(self, chunk_size):
return ((line, b'\n') for line in self._orig.iter_lines(chunk_size)) return ((line, b'\n') for line in self._orig.iter_lines(chunk_size))
# noinspection PyProtectedMember
@property @property
def headers(self): def headers(self):
try:
raw = self._orig.raw
if getattr(raw, '_original_response', None):
raw_version = raw._original_response.version
else:
raw_version = raw.version
except AttributeError:
# Assume HTTP/1.1
raw_version = 11
version = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2.0',
}[raw_version]
original = self._orig original = self._orig
status_line = f'HTTP/{version} {original.status_code} {original.reason}' status_line = f'HTTP/{self.version} {original.status_code} {original.reason}'
headers = [status_line] headers = [status_line]
headers.extend( headers.extend(
': '.join(header) ': '.join(header)
@ -117,6 +99,32 @@ class HTTPResponse(HTTPMessage):
for key, value in data.items() for key, value in data.items()
) )
@property
def version(self) -> str:
"""
Return the HTTP version used by the server, e.g. '1.1'.
Assume HTTP/1.1 if version is not available.
"""
mapping = {
9: '0.9',
10: '1.0',
11: '1.1',
20: '2.0',
}
fallback = 11
version = None
try:
raw = self._orig.raw
if getattr(raw, '_original_response', None):
version = raw._original_response.version
else:
version = raw.version
except AttributeError:
pass
return mapping[version or fallback]
class HTTPRequest(HTTPMessage): class HTTPRequest(HTTPMessage):
"""A :class:`requests.models.Request` wrapper.""" """A :class:`requests.models.Request` wrapper."""

View File

@ -24,7 +24,7 @@ def enable_highlighter(
console: Console, console: Console,
highlighter: Highlighter, highlighter: Highlighter,
) -> Iterator[Console]: ) -> Iterator[Console]:
"""Enable a higlighter temporarily.""" """Enable a highlighter temporarily."""
original_highlighter = console.highlighter original_highlighter = console.highlighter
try: try:

View File

@ -245,7 +245,7 @@ def get_site_paths(path: Path) -> Iterable[Path]:
yield as_site(path) yield as_site(path)
def split(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]: def split_iterable(iterable: Iterable[T], key: Callable[[T], bool]) -> Tuple[List[T], List[T]]:
left, right = [], [] left, right = [], []
for item in iterable: for item in iterable:
if key(item): if key(item):

View File

@ -5,7 +5,7 @@ import responses
from httpie.cli.constants import PRETTY_MAP from httpie.cli.constants import PRETTY_MAP
from httpie.cli.exceptions import ParseError from httpie.cli.exceptions import ParseError
from httpie.cli.nested_json import HTTPieSyntaxError from httpie.cli.nested_json import NestedJSONSyntaxError
from httpie.output.formatters.colors import ColorFormatter from httpie.output.formatters.colors import ColorFormatter
from httpie.utils import JsonDictPreservingDuplicateKeys from httpie.utils import JsonDictPreservingDuplicateKeys
@ -157,7 +157,7 @@ def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
f'option:={json.dumps(value)}', f'option:={json.dumps(value)}',
) )
cm.match("Can't use complex JSON value types") cm.match('Cannot use complex JSON value types')
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -508,23 +508,23 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
), ),
( (
['foo=1', 'foo[key]:=2'], ['foo=1', 'foo[key]:=2'],
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^", "HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'string' but this operation requires a type of 'object'.\nfoo[key]\n ^^^^^",
), ),
( (
['foo=1', 'foo[0]:=2'], ['foo=1', 'foo[0]:=2'],
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^", "HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[0]\n ^^^",
), ),
( (
['foo=1', 'foo[]:=2'], ['foo=1', 'foo[]:=2'],
"HTTPie Type Error: Can't perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^", "HTTPie Type Error: Cannot perform 'append' based access on 'foo' which has a type of 'string' but this operation requires a type of 'array'.\nfoo[]\n ^^",
), ),
( (
['data[key]=value', 'data[key 2]=value 2', 'data[0]=value'], ['data[key]=value', 'data[key 2]=value 2', 'data[0]=value'],
"HTTPie Type Error: Can't perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^", "HTTPie Type Error: Cannot perform 'index' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[0]\n ^^^",
), ),
( (
['data[key]=value', 'data[key 2]=value 2', 'data[]=value'], ['data[key]=value', 'data[key 2]=value 2', 'data[]=value'],
"HTTPie Type Error: Can't perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^", "HTTPie Type Error: Cannot perform 'append' based access on 'data' which has a type of 'object' but this operation requires a type of 'array'.\ndata[]\n ^^",
), ),
( (
[ [
@ -532,7 +532,7 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
'foo[bar][baz][5][]:=4', 'foo[bar][baz][5][]:=4',
'foo[bar][baz][key][]:=5', 'foo[bar][baz][key][]:=5',
], ],
"HTTPie Type Error: Can't perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^", "HTTPie Type Error: Cannot perform 'key' based access on 'foo[bar][baz]' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[bar][baz][key][]\n ^^^^^",
), ),
( (
['foo[-10]:=[1,2]'], ['foo[-10]:=[1,2]'],
@ -540,32 +540,32 @@ def test_nested_json_syntax(input_json, expected_json, httpbin):
), ),
( (
['foo[0]:=1', 'foo[]:=2', 'foo[\\2]:=3'], ['foo[0]:=1', 'foo[]:=2', 'foo[\\2]:=3'],
"HTTPie Type Error: Can't perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^", "HTTPie Type Error: Cannot perform 'key' based access on 'foo' which has a type of 'array' but this operation requires a type of 'object'.\nfoo[\\2]\n ^^^^",
), ),
( (
['foo[\\1]:=2', 'foo[5]:=3'], ['foo[\\1]:=2', 'foo[5]:=3'],
"HTTPie Type Error: Can't perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^", "HTTPie Type Error: Cannot perform 'index' based access on 'foo' which has a type of 'object' but this operation requires a type of 'array'.\nfoo[5]\n ^^^",
), ),
( (
['x=y', '[]:=2'], ['x=y', '[]:=2'],
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.", "HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
), ),
( (
['[]:=2', 'x=y'], ['[]:=2', 'x=y'],
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.", "HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
), ),
( (
[':=[1,2,3]', '[]:=4'], [':=[1,2,3]', '[]:=4'],
"HTTPie Type Error: Can't perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.", "HTTPie Type Error: Cannot perform 'append' based access on '' which has a type of 'object' but this operation requires a type of 'array'.",
), ),
( (
['[]:=4', ':=[1,2,3]'], ['[]:=4', ':=[1,2,3]'],
"HTTPie Type Error: Can't perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.", "HTTPie Type Error: Cannot perform 'key' based access on '' which has a type of 'array' but this operation requires a type of 'object'.",
), ),
], ],
) )
def test_nested_json_errors(input_json, expected_error, httpbin): def test_nested_json_errors(input_json, expected_error, httpbin):
with pytest.raises(HTTPieSyntaxError) as exc: with pytest.raises(NestedJSONSyntaxError) as exc:
http(httpbin + '/post', *input_json) http(httpbin + '/post', *input_json)
exc_lines = str(exc.value).splitlines() exc_lines = str(exc.value).splitlines()