forked from extern/httpie-cli
Compare commits
24 Commits
fix-initia
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
af5e9897c9 | |||
47e9b99ba1 | |||
265841f866 | |||
b16392fbb9 | |||
e73c3e6c24 | |||
f0563deb7f | |||
4894b4c0fc | |||
3a123c4125 | |||
621042a048 | |||
0689b55e1d | |||
a7321d8ac4 | |||
d9a73cd8eb | |||
930cd9081a | |||
3549ee8342 | |||
810bb1c77b | |||
767f3c3a19 | |||
1236793272 | |||
c3a2f87dd2 | |||
1121d695a8 | |||
5794a070e1 | |||
4736a16698 | |||
3ad408add7 | |||
91cdb22a4b | |||
c995fd9b24 |
4
.github/workflows/autogenerated-files.yml
vendored
4
.github/workflows/autogenerated-files.yml
vendored
@ -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"
|
||||||
|
2
.github/workflows/benchmark.yml
vendored
2
.github/workflows/benchmark.yml
vendored
@ -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"
|
||||||
|
|
||||||
|
2
.github/workflows/code-style.yml
vendored
2
.github/workflows/code-style.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/release-brew.yml
vendored
2
.github/workflows/release-brew.yml
vendored
@ -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 }}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
2
.github/workflows/release-pypi.yml
vendored
2
.github/workflows/release-pypi.yml
vendored
@ -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
|
||||||
|
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -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'
|
||||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
2
Makefile
2
Makefile
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
52
README.md
52
README.md
@ -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">
|
||||||
|
|
||||||
|
[](https://httpie.io/product)
|
||||||
|
[](https://httpie.io/app)
|
||||||
|
[](https://httpie.io/cli)
|
||||||
|
[](https://twitter.com/httpie)
|
||||||
|
[](https://httpie.io/discord)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://httpie.org/docs/cli)
|
||||||
|
[](https://pypi.python.org/pypi/httpie)
|
||||||
|
[](https://github.com/httpie/httpie/actions)
|
||||||
|
[](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.
|
||||||
|
|
||||||
[](https://httpie.org/docs)
|
<div align="center">
|
||||||
[](https://pypi.python.org/pypi/httpie)
|
|
||||||
[](https://github.com/httpie/httpie/actions)
|
|
||||||
[](https://codecov.io/gh/httpie/httpie)
|
|
||||||
[](https://twitter.com/httpie)
|
|
||||||
[](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)
|
||||||
|
@ -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 host’s SSL certificate verification, you can pass `--verify=no` (default is `yes`):
|
|
||||||
|
To skip the host’s 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).
|
||||||
|
|
||||||
|
@ -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>.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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']
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -92,7 +92,3 @@ class MultipartRequestDataDict(MultiValueOrderedDict):
|
|||||||
|
|
||||||
class RequestFilesDict(RequestDataDict):
|
class RequestFilesDict(RequestDataDict):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NestedJSONArray(list):
|
|
||||||
"""Denotes a top-level JSON array."""
|
|
||||||
|
@ -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)
|
|
20
httpie/cli/nested_json/__init__.py
Normal file
20
httpie/cli/nested_json/__init__.py
Normal 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'
|
||||||
|
]
|
27
httpie/cli/nested_json/errors.py
Normal file
27
httpie/cli/nested_json/errors.py
Normal 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)
|
129
httpie/cli/nested_json/interpret.py
Normal file
129
httpie/cli/nested_json/interpret.py
Normal 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 that’s 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
|
193
httpie/cli/nested_json/parse.py
Normal file
193
httpie/cli/nested_json/parse.py
Normal 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')
|
80
httpie/cli/nested_json/tokens.py
Normal file
80
httpie/cli/nested_json/tokens.py
Normal 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."""
|
@ -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 there’s 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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user