Repository: httpie/http-prompt Branch: master Commit: 6602b8151685 Files: 54 Total size: 221.5 KB Directory structure: gitextract_2twf_0e5/ ├── .coveragerc ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs/ │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── _templates/ │ │ └── layout.html │ ├── conf.py │ ├── contributor-guide.rst │ ├── index.rst │ ├── make.bat │ └── user-guide.rst ├── http_prompt/ │ ├── __init__.py │ ├── cli.py │ ├── completer.py │ ├── completion.py │ ├── config.py │ ├── context/ │ │ ├── __init__.py │ │ └── transform.py │ ├── contextio.py │ ├── defaultconfig.py │ ├── execution.py │ ├── lexer.py │ ├── options.py │ ├── output.py │ ├── tree.py │ ├── utils.py │ └── xdg.py ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── snap/ │ └── snapcraft.yaml ├── tests/ │ ├── __init__.py │ ├── base.py │ ├── context/ │ │ ├── test_context.py │ │ └── test_transform.py │ ├── test_cli.py │ ├── test_completer.py │ ├── test_config.py │ ├── test_contextio.py │ ├── test_execution.py │ ├── test_installation.py │ ├── test_interaction.py │ ├── test_lexer.py │ ├── test_tree.py │ ├── test_utils.py │ ├── test_xdg.py │ └── utils.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [report] show_missing = True exclude_lines = nocover ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - run: python -m pip install -U pip setuptools wheel - run: python -m pip install -r requirements-test.txt - run: python -m pip install -e . - run: python -m pytest ================================================ FILE: .gitignore ================================================ *.egg-info *.pyc .cache .coverage .DS_Store .python-version .tox _build build dist data.json venv* ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016-2021 Chang-Hung Liang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include README.rst LICENSE requirements.txt requirements-test.txt ================================================ FILE: Makefile ================================================ .PHONY: build install: python -m pip install -e . python -m pip install -r requirements-test.txt clean: rm -rf dist/ build/ test: python -m pytest build: python setup.py sdist bdist_wheel check: twine check dist/* upload: twine upload --repository=http-prompt dist/* release: test clean build check upload ================================================ FILE: README.rst ================================================ HTTP Prompt =========== |PyPI| |Docs| |Build| |Coverage| |Discord| HTTP Prompt is an interactive command-line HTTP client featuring autocomplete and syntax highlighting, built on HTTPie_ and prompt_toolkit_. |Asciinema| Links ----- * Home: https://http-prompt.com * Documentation: https://docs.http-prompt.com * Code: https://github.com/httpie/http-prompt * Chat: https://httpie.io/chat .. |PyPI| image:: https://img.shields.io/pypi/v/http-prompt.svg :target: https://pypi.python.org/pypi/http-prompt .. |Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat :target: http://docs.http-prompt.com/en/latest/?badge=latest .. |Build| image:: https://github.com/httpie/http-prompt/workflows/Build/badge.svg :target: https://github.com/httpie/http-prompt/actions .. |Coverage| image:: https://coveralls.io/repos/github/eliangcs/http-prompt/badge.svg?branch=master :target: https://coveralls.io/github/eliangcs/http-prompt?branch=master .. |Discord| image:: https://img.shields.io/badge/chat-on%20Discord-brightgreen?style=flat-square :target: https://httpie.io/chat .. |Asciinema| image:: https://asciinema.org/a/96613.png :target: https://asciinema.org/a/96613?theme=monokai&size=medium&autoplay=1&speed=1.5 .. _HTTPie: https://httpie.io .. _prompt_toolkit: https://github.com/jonathanslenders/python-prompt-toolkit ================================================ FILE: docs/LICENSE ================================================ Refer to LICENSE in the main repo: https://github.com/httpie/http-prompt ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = HTTPPrompt SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/README.md ================================================ # HTTP Prompt Documentation This repo contains the documentation for HTTP Prompt, published on http://docs.http-prompt.com. The source code of HTTP Prompt can be found in the main repo: https://github.com/httpie/http-prompt ## How to Build ``` pip install sphinx make html open _build/html/index.html ``` ================================================ FILE: docs/_templates/layout.html ================================================ {%- extends "!layout.html" %} ================================================ FILE: docs/conf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # HTTP Prompt documentation build configuration file, created by # sphinx-quickstart on Wed Dec 21 20:28:44 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) from collections import OrderedDict from http_prompt import __version__ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'HTTP Prompt' copyright = '2016-17, Chang-Hung Liang' author = 'Chang-Hung Liang' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'HTTPPromptdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'HTTPPrompt.tex', 'HTTP Prompt Documentation', 'Chang-Hung Liang', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'httpprompt', 'HTTP Prompt Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'HTTPPrompt', 'HTTP Prompt Documentation', author, 'HTTPPrompt', 'One line description of project.', 'Miscellaneous'), ] html_sidebars = { '**': [ 'localtoc.html', 'navigation.html' ] } html_theme_options = { 'extra_nav_links': OrderedDict([ ('Home', 'http://http-prompt.com'), ('Discord', 'https://httpie.io/chat'), ('Code on GitHub', 'https://github.com/httpie/http-prompt'), ]) } ================================================ FILE: docs/contributor-guide.rst ================================================ .. _contributor-guide: Contributor Guide ================= This document is for developers who want to contribute code to this project. Any contributions are welcome and greatly appreciated! This project follows the common conventions of a Python/GitHub project. So if you're already an experienced Python/GitHub user, it should be straightforward for you to set up your development environment and send patches. Generally, the steps include: 1. Fork and clone the repo 2. Create a virtualenv for this project 3. Install dependent packages with ``pip install -e .`` 4. Install test dependent packages with ``pip install -r requirements-test.txt`` 5. Make your changes to the code 6. Run tests with ``pytest`` and ``tox`` 7. Commit and push your changes 8. Send a pull request 9. Wait to be reviewed and get merged! If you're not familiar with any of the above steps, read the following instructions. Forking ------- Fork_ is like copying someone else's project to your account, so you can start your own independent development without interfering with the original one. To fork HTTP Prompt, just click the **Fork** button on HTTP Prompt's GitHub project page. Then you clone your fork to your local computer:: $ cd ~/Projects $ git clone git@github.com:{YOUR_USERNAME}/http-prompt.git Read `Forking Projects`_ on GitHub to learn more. Working with virtualenv ----------------------- *virtualenv* is the de facto standard tool when developing a Python project. Instead of polluting your system-wide Python installation with different Python projects, virtualenv creates an isolated Python environment exclusively for a Python project. There are several tools you can use for managing virtualenvs. In this guide, we'll show you how to use pyenv_ and pyenv-virtualenv_, which is one of the most popular virtualenv management tools. Make sure you have installed pyenv_ and pyenv-virtualenv_ first. HTTP Prompt should work on Python 3.6 and newer. You can use any of these Python versions as your development environment, but using the latest version (3.6.x) is probably the best. You can install the latest Python with pyenv:: $ pyenv install 3.6.0 This will install Python 3.6.0 in ``~/.pyenv/versions/3.6.0`` directory. To create a virtualenv for HTTP Prompt, do:: $ pyenv virtualenv 3.6.0 http-prompt The command means: create a virtualenv named "http-prompt" based on Python 3.6.0. The virtualenv can be found at ``~/.pyenv/versions/3.6.0/envs/http-prompt``. To activate the virtualenv, do:: $ pyenv activate http-prompt This will switch your Python environment from the system-wide Python to the virtualenv's (named "http-prompt") Python. To go back to the system-wide Python, you have to deactivate the virtualenv:: $ pyenv deactivate Refer to pyenv_ and pyenv-virtualenv_ if anything else is unclear. Installing Dependent Packages ----------------------------- The dependent packages should be installed on a virtualenv, so make sure you activate your virtualenv first. If not, do:: $ pyenv activate http-prompt It is also recommended to use the latest version of pip. You can upgrade it with:: $ pip install -U pip Install HTTP Prompt with its dependent packages:: $ cd ~/Projects/http-prompt $ pip install -e . ``pip install -e .`` means install the ``http-prompt`` package in editable mode (or developer mode). This allows you to edit code directly in ``~/Projects/http-prompt`` without reinstalling the package. Without the ``-e`` option, the package will be installed to Python's ``site-packages`` directory, which is not convenient for developing. Installing Test Dependent Packages ---------------------------------- Test requirements are placed in a separate file named ``requirements-test.txt``. To install them, do:: $ cd ~/Projects/http-prompt $ pip install -r requirements-test.txt Making Your Changes ------------------- Code Style ~~~~~~~~~~ Always lint your code with Flake8_. You can set it up in your code editor or simply use ``flake8`` in the command line. `The Hitchhiker’s Guide to Python`_ provides the best Python coding practices. We recommend anyone who wants to write good Python code to read it. Adding Features ~~~~~~~~~~~~~~~ Before you add a new feature, make sure you create an issue making a proposal first, because you don't want to waste your time on something that the community don't agree upon. Python Compatibility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ HTTP Prompt is compatible with Python 3.6+. Documentation ~~~~~~~~~~~~~ Documentation is written in Sphinx_. To build documentation, you need to install Sphinx_ first:: $ pip install sphinx To build and view documentation in HTML, do:: $ cd ~/Projects/http-prompt/docs $ make html $ open _build/html/index.html Running Tests ------------- Single Python Version ~~~~~~~~~~~~~~~~~~~~~ Make sure your virtualenv is activated. To run tests, do:: $ cd ~/Projects/http-prompt $ pytest ``pytest`` runs the tests with your virtualenv's Python version. This is good for fast testing. To test the code against multiple Python versions, you use Tox_. Multiple Python Versions ~~~~~~~~~~~~~~~~~~~~~~~~ All the commands in this section should **NOT** be run in a virtualenv. Deactivate it first if you're in a virtualenv:: $ pyenv deactivate Make sure you have installed all the Python versions we're targeting. If not, do:: $ pyenv install 3.6.0 $ pyenv install 3.7.0 $ pyenv install 3.8.0 To use Tox_ with pyenv_, you have to instruct pyenv to use multiple Python versions for the project:: $ cd ~/Projects/http-prompt $ pyenv local 3.6.0 3.7.0 3.8.0 This will generate a ``.python-version`` in the project directory:: $ cat ~/Projects/http-prompt/.python-version 3.6.0 3.7.0 3.8.0 This tells pyenv_ to choose a Python version based on the above order. In this case, 3.6.0 is the first choice, so any Python executables (such as ``python`` and ``pip``) will be automatically mapped to the ones in ``~/.pyenv/versions/3.8.0/bin``. We want to run ``tox`` using on Python 3.8.0. Make sure you have installed Tox_:: $ pip install tox To run tests, execute ``tox``:: $ cd ~/Projects/http-prompt $ tox Tox_ will install the test Python environments in the ``.tox/`` directory in the project directory, and run the test code against all the Python versions listed above. Code Review ----------- Once you made changes and all the tests pass, push your modified code to your GitHub account. Submit a pull request (PR) on GitHub for the maintainers to review. If the patch is good, The maintainers will merge it to the master branch and ship the new code in the next release. If the patch needs improvements, we'll give you feedback so you can modify accordingly and resubmit it to the PR. .. _Flake8: http://flake8.pycqa.org/en/latest/index.html .. _Fork: https://en.wikipedia.org/wiki/Fork_(software_development) .. _Forking Projects: https://guides.github.com/activities/forking/ .. _pyenv-virtualenv: https://github.com/yyuu/pyenv-virtualenv .. _pyenv: https://github.com/yyuu/pyenv .. _Sphinx: http://www.sphinx-doc.org/ .. _The Hitchhiker’s Guide to Python: http://docs.python-guide.org/en/latest/ .. _Tox: https://tox.readthedocs.io/en/latest/ ================================================ FILE: docs/index.rst ================================================ HTTP Prompt Documentation ========================= HTTP Prompt is an interactive command-line HTTP client featuring autocomplete and syntax highlighting, built on HTTPie_ and prompt_toolkit_. See it in action: |Asciinema| Contents -------- .. toctree:: :maxdepth: 3 user-guide contributor-guide Roadmap ------- * Support for advanced HTTPie syntax, e.g, ``field=@file.json`` * Support for cURL command and raw format preview * Improve autocomplete * Python syntax evaluation * HTTP/2 support User Support ------------ We'd love to hear more from our users! Please use the following channels for bug reports, feature requests, and questions: * `GitHub issues`_ * `Gitter chat room`_ Contributing ------------ Are you a developer and interested in contributing to HTTP Prompt? See :ref:`Contributor Guide `. Thanks ------ * HTTPie_: for designing such a user-friendly HTTP CLI * prompt_toolkit_: for simplifying the work of building an interactive CLI * Parsimonious_: for the PEG parser used by this project * pgcli_: for the inspiration of this project * Contributors_: for improving this project .. |Asciinema| image:: https://asciinema.org/a/96613.png :target: https://asciinema.org/a/96613?theme=monokai&size=medium&autoplay=1&speed=1.5 .. _Contributors: https://github.com/eliangcs/http-prompt/graphs/contributors .. _GitHub issues: https://github.com/httpie/http-prompt/issues .. _Discord: https://htpie.io/chat .. _HTTPie: https://httpie.io .. _Parsimonious: https://github.com/erikrose/parsimonious .. _pgcli: http://pgcli.com .. _prompt_toolkit: https://github.com/jonathanslenders/python-prompt-toolkit ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=HTTPPrompt if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: docs/user-guide.rst ================================================ .. _user-guide: User Guide ========== Installation ------------ Just install it like a regular Python package:: $ pip install http-prompt You'll probably see some permission errors if you're trying to install it on the system-wide Python. It isn't recommended. But if that's what you want to do, you need to ``sudo``:: $ sudo pip install http-prompt Another alternative is to use ``--user`` option to install the package into your user directory:: $ pip install --user http-prompt To upgrade HTTP Prompt, do:: $ pip install -U http-prompt It's also possible to install it using Homebrew:: $ brew install http-prompt Quickstart ---------- To start a session, you use the ``http-prompt`` executable: .. code-block:: bash # Start with the last session or http://localhost:8000 $ http-prompt # Start with the given URL $ http-prompt http://httpbin.org # Start with some initial options $ http-prompt localhost:8000/api --auth user:pass username=somebody Once you're in a session, you can use the following commands. To change URL address, use ``cd``: .. code-block:: bash # Relative URL path > cd api/v1 # Absolute URL > cd http://localhost/api To add headers, querystring, or body parameters, use the syntax as in HTTPie_. The following are all valid: .. code-block:: bash # Header > Content-Type:application/json # Querystring parameter > page==2 # Body parameters > username=foo > full_name='foo bar' # Body parameters in raw JSON (new in v0.9.0) > number:=1234 > is_ok:=true > names:=["foo","bar"] > user:='{"username": "foo", "password": "bar"}' # Write them in one line > Content-Type:application/json page==2 username=foo You can also add HTTPie_ options like this: .. code-block:: bash > --form --auth user:pass > --verify=no # HTTPie options and request parameters in one line > --form --auth user:pass username=foo Content-Type:application/json To preview how HTTP Prompt is going to call HTTPie_, do: .. code-block:: bash > httpie post http --auth user:pass --form POST http://localhost/api apikey==abc username=john You can temporarily override the request parameters by supplying options and parameters in ``httpie`` command. The overrides won't affect the later requests. .. code-block:: bash # No parameters initially > httpie http http://localhost # Override parameters temporarily > httpie /api/something page==2 --json http --json http://localhost/api/something page==2 # Current state is not affected by the above overrides > httpie http http://localhost Since v0.6.0, apart from ``httpie`` command, you can also use ``env`` to print the current session: .. code-block:: bash > env --verify=no cd http://localhost page==10 limit==20 To actually send an HTTP request, enter one of the HTTP methods: .. code-block:: bash > get > post > put > patch > delete > head > options (new in v0.8.0) The above HTTP methods also support temporary overriding: .. code-block:: bash # No parameters initially > httpie http http://localhost # Send a request with some overrided parameters > post /api/v1 --form name=jane # Current state remains intact > httpie http http://localhost To remove an existing header, a querystring parameter, a body parameter, or an HTTPie_ option: .. code-block:: bash # Remove a header > rm -h Content-Type # Remove a querystring parameter > rm -q apikey # Remove a body parameter > rm -b username # Remove an HTTPie option > rm -o --auth To reset the session, i.e., clear all parameters and options: .. code-block:: bash > rm * To exit a session, simply enter: .. code-block:: bash > exit Output Redirection ------------------ *New in v0.6.0.* You can redirect the output of a command to a file by using the syntax: .. code-block:: bash # Write output to a file > COMMAND > /path/to/file # Append output to a file > COMMAND >> /path/to/file where ``COMMAND`` can be one of the following: * ``env`` * ``httpie`` * HTTP actions: ``get``, ``post``, ``put``, ``patch``, ``delete``, ``head``, ``options`` Saving and Loading Sessions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ One of the use cases of output redirection is to save and load sessions, which is especially useful for team collaboration, where you want to share your sessions with your team members. To save your current session, you redirect the output of ``env`` to a file: .. code-block:: bash > env > /path/to/file To load a saved session, you can use ``source`` or ``exec``. Their only difference is that ``exec`` wipes out the current session before loading. Usage: .. code-block:: bash # Update the current session > source /path/to/file # Wipe out the current session and load from a file > exec /path/to/file *New in v0.11.0.* Load a saved session from the command line directly with the ``--env`` option. This allows you for example to define aliases and easily start HTTP Prompt with a full configuration already loaded for each of your projects. .. code-block:: bash # Define alias for project1 $ alias http_project1='http-prompt --env /path/to/project1/env/file' # Launch HTTP Prompt for project1 $ http_project1 Any extra argument in the command line is still used and overwrites the value from the session file if already present .. code-block:: bash # Use saved session but overwrite the URL and add a parameter $ http-prompt --env /path/to/file localhost:8080 page==2 Saving HTTP Responses ~~~~~~~~~~~~~~~~~~~~~ Printing HTTP responses to the console is good for small text responses. For larger text or binary data, you may want to save the response to a file. Usage: .. code-block:: bash # Save http://httpbin.org/image/png to a file > cd http://httpbin.org/image/png > get > pig.png # Or use this one-liner > get http://httpbin.org/image/png > pig.png Pipeline -------- *New in v0.7.0.* HTTP Prompt supports simplified pipeline syntax, where you can pipe the output to a shell command: .. code-block:: bash # Replace 'localhost' to '127.0.0.1' > httpie POST http://localhost | sed 's/localhost/127.0.0.1/' http http://127.0.0.1 # Only print the line that contains 'User-Agent' using grep > get http://httpbin.org/get | grep 'User-Agent' "User-Agent": "HTTPie/0.9.6" On macOS, you can even copy the result to the clipboard using ``pbcopy``: .. code-block:: bash # Copy the HTTPie command to the clipboard (macOS only) > httpie | pbcopy Another cool trick is to use jq_ to parse JSON data: .. code-block:: bash > get http://httpbin.org/get | jq '.headers."User-Agent"' "HTTPie/0.9.6" **Note**: Syntax with multiple pipes is not supported currently. Shell Substitution ------------------ *New in v0.7.0.* Shell substitution happens when you put a shell command between two backticks like ```...```. This syntax allows you compute a value from the shell environment and assign the value to a parameter:: # Set date to current time > date==`date -u +"%Y-%m-%d %H:%M:%S"` > httpie http http://localhost:8000 'date==2016-10-08 09:45:00' # Get password from a file. Suppose the file has a content of # "secret_api_key". > password==`cat ./apikey.txt` > httpie http http://localhost:8000 password==secret_api_key Configuration ------------- *New in v0.4.0.* When launched for the first time, HTTP Prompt creates a user config file at ``$XDG_CONFIG_HOME/http-prompt/config.py`` (or ``%LOCALAPPDATA%/http-prompt/config.py`` on Windows). By default, it's ``~/.config/http-prompt/config.py`` (or ``~/AppData/Local/http-prompt/config.py``). ``config.py`` is a Python module with all the available options you can customize. Don't worry. You don't need to know Python to edit it. Just open it up with a text editor and follow the guidance inside. Persistent Context ------------------ *New in v0.4.0.* HTTP Prompt keeps a data structure called *context* to represent your current session. Every time you enter a command modifying your context, HTTP Prompt saves the context to your filesystem, enabling you to resume your previous session when you restart ``http-prompt``. The last saved context is located at ``$XDG_DATA_HOME/http-prompt/context.hp`` (or ``%LOCALAPPDATA%/http-prompt/context.hp`` on Windows). By default, it's ``~/.local/share/http-prompt/context.hp`` (or ``~/AppData/Local/http-prompt/context.hp``). As context data may contain sensitive data like API keys, you should keep the user data directory private. By default, HTTP Prompt sets the modes of ``$XDG_DATA_HOME/http-prompt`` to ``rwx------`` (i.e., ``700``) so that the only person who can read it is the owner (you). **Note for users of older versions**: Since 0.6.0, HTTP Prompt only stores the last context instead of grouping multiple contexts by hostnames and ports like it did previously. We changed the behavior because the feature can be simply replaced by ``env``, ``exec`` and ``source`` commands. See the discussion in `issue #70 `_ for detail. ``ls``, ``cd``, and OpenAPI/Swagger Specification ------------------------------------------------- *New in v0.10.0.* OpenAPI_ (formerly known as Swagger_) is a specification that describes an HTTP/REST API. The ``http-prompt`` has a ``--spec`` option for you to provide an OpenAPI specification in JSON format. The specification enables HTTP Prompt to do some cool things like autocomplete API endpoint paths and parameters for you. See it in action: |ls-demo| To use this feature, specify an OpenAPI/Swagger specification file with ``--spec`` command line option:: # Specify a spec on local filesystem $ http-prompt http://localhost:8000 --spec /path/to/spec.json # Specify a spec on the internet (https://apis.guru has lots of them) $ http-prompt https://api.github.com --spec https://api.apis.guru/v2/specs/github.com/v3/swagger.json Then you can use ``ls`` and ``cd`` commands to navigate API endpoints with autocomplete! .. |ls-demo| image:: https://asciinema.org/a/107732.png :target: https://asciinema.org/a/107732 .. _HTTPie: https://httpie.org .. _jq: https://stedolan.github.io/jq/ .. _OpenAPI: https://openapis.org .. _Swagger: http://swagger.io/ ================================================ FILE: http_prompt/__init__.py ================================================ __version__ = '2.1.0' ================================================ FILE: http_prompt/cli.py ================================================ import json from http.cookies import SimpleCookie from urllib.request import pathname2url, urlopen import yaml import os import re import sys import click from httpie.plugins import FormatterPlugin # noqa, avoid cyclic import from httpie.output.formatters.colors import Solarized256Style from prompt_toolkit import prompt from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.history import FileHistory from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles.pygments import style_from_pygments_cls from pygments.styles import get_style_by_name from pygments.util import ClassNotFound from . import __version__ from . import config from .completer import HttpPromptCompleter from .context import Context from .contextio import load_context, save_context from .execution import execute from .lexer import HttpPromptLexer from .utils import smart_quote from .xdg import get_data_dir def fix_incomplete_url(url): if url.startswith(('s://', '://')): url = 'http' + url elif url.startswith('//'): url = 'http:' + url elif not url.startswith(('http://', 'https://')): url = 'http://' + url return url def update_cookies(base_value, cookies): cookie = SimpleCookie(base_value) for k, v in cookies.items(): cookie[k] = v return str(cookie.output(header='', sep=';').lstrip()) class ExecutionListener(object): def __init__(self, cfg): self.cfg = cfg def context_changed(self, context): # Dump the current context to HTTP Prompt format save_context(context) def response_returned(self, context, response): if not response.cookies: return cookie_pref = self.cfg.get('set_cookies') or 'auto' if cookie_pref == 'auto' or ( cookie_pref == 'ask' and click.confirm('Cookies incoming! Do you want to set them?')): existing_cookie = context.headers.get('Cookie') new_cookie = update_cookies(existing_cookie, response.cookies) context.headers['Cookie'] = new_cookie click.secho('Cookies set: %s' % new_cookie) def normalize_url(ctx, param, value): if value: if not re.search(r'^\w+://', value): value = 'file:' + pathname2url(os.path.abspath(value)) return value return None @click.command(context_settings={'ignore_unknown_options': True}) @click.option('--spec', help='OpenAPI/Swagger specification file.', callback=normalize_url) @click.option('--env', help='Environment file to preload.', type=click.Path(exists=True)) @click.argument('url', default='') @click.argument('http_options', nargs=-1, type=click.UNPROCESSED) @click.version_option(message='%(version)s') def cli(spec, env, url, http_options): click.echo('Version: %s' % __version__) copied, config_path = config.initialize() if copied: click.echo('Config file not found. Initialized a new one: %s' % config_path) cfg = config.load() # Override pager/less options os.environ['PAGER'] = cfg['pager'] os.environ['LESS'] = '-RXF' if spec: f = urlopen(spec) try: content = f.read().decode() try: spec = json.loads(content) except json.JSONDecodeError: try: spec = yaml.safe_load(content) except yaml.YAMLError: click.secho("Warning: Specification file '%s' is neither valid JSON nor YAML" % spec, err=True, fg='red') spec = None finally: f.close() if url: url = fix_incomplete_url(url) context = Context(url, spec=spec) output_style = cfg.get('output_style') if output_style: context.options['--style'] = output_style # For prompt-toolkit history = FileHistory(os.path.join(get_data_dir(), 'history')) lexer = PygmentsLexer(HttpPromptLexer) completer = HttpPromptCompleter(context) try: style_class = get_style_by_name(cfg['command_style']) except ClassNotFound: style_class = Solarized256Style style = style_from_pygments_cls(style_class) listener = ExecutionListener(cfg) if len(sys.argv) == 1: # load previous context if nothing defined load_context(context) else: if env: load_context(context, env) if url: # Overwrite the env url if not default context.url = url if http_options: # Execute HTTPie options from CLI (can overwrite env file values) http_options = [smart_quote(a) for a in http_options] execute(' '.join(http_options), context, listener=listener) while True: try: text = prompt('%s> ' % context.url, completer=completer, lexer=lexer, style=style, history=history, auto_suggest=AutoSuggestFromHistory(), vi_mode=cfg['vi']) except KeyboardInterrupt: continue # Control-C pressed except EOFError: break # Control-D pressed else: execute(text, context, listener=listener, style=style_class) if context.should_exit: break click.echo('Goodbye!') ================================================ FILE: http_prompt/completer.py ================================================ # -*- coding: utf-8 -*- import re from collections import OrderedDict from itertools import chain from urllib.parse import urlparse from prompt_toolkit.completion import Completer, Completion from .completion import (ROOT_COMMANDS, ACTIONS, OPTION_NAMES, HEADER_NAMES, HEADER_VALUES) RULES = [ # (regex pattern, a method name in CompletionGenerator) (r'((?:[^\s\'"\\=:]|(?:\\.))+):((?:[^\s\'"\\]|(?:\\.))*)$', 'header_values'), (r'(get|head|post|put|patch|delete|connect)\s+', 'concat_mutations'), (r'(httpie|curl)\s+', 'preview'), (r'rm\s+\-b\s+', 'existing_body_params'), (r'rm\s+\-h\s+', 'existing_header_names'), (r'rm\s+\-o\s+', 'existing_option_names'), (r'rm\s+\-q\s+', 'existing_querystring_params'), # The last two captures are full URL path and the last part of the URL # path. For example: # '/foo/bar' => ('/foo/bar', 'bar') # '/foo/bar/' => ('/foo/bar/', '') # 'foo/bar' => ('foo/bar', 'bar') (r'(ls|cd)\s+(/?(?:[^/]+/)*([^/]*)/?)$', 'urlpaths'), (r'^\s*[^\s]*$', 'root_commands') ] def compile_rules(rules): compiled_rules = [] for pattern, meta_dict in rules: regex = re.compile(pattern) compiled_rules.append((regex, meta_dict)) return compiled_rules RULES = compile_rules(RULES) def fuzzyfinder(text, collection): """https://github.com/amjith/fuzzyfinder""" suggestions = [] if not isinstance(text, str): text = str(text) pat = '.*?'.join(map(re.escape, text)) regex = re.compile(pat, flags=re.IGNORECASE) for item in collection: r = regex.search(item) if r: suggestions.append((len(r.group()), r.start(), item)) return (z for _, _, z in sorted(suggestions)) def match_completions(cur_word, word_dict): words = word_dict.keys() suggestions = fuzzyfinder(cur_word, words) for word in suggestions: desc = word_dict.get(word, '') yield Completion(word, -len(cur_word), display_meta=desc) class CompletionGenerator(object): def root_commands(self, context, match): return chain( self._generic_generate(ROOT_COMMANDS.keys(), {}, ROOT_COMMANDS), self.actions(context, match), self.concat_mutations(context, match) ) def header_values(self, context, match): header_name = match.group(1) header_values = HEADER_VALUES.get(header_name) if header_values: for value in header_values: yield value, header_name def preview(self, context, match): return chain( self.actions(context, match), self.concat_mutations(context, match) ) def actions(self, context, match): return self._generic_generate(ACTIONS.keys(), {}, ACTIONS) def concat_mutations(self, context, match): return chain( self._generic_generate(context.body_params.keys(), context.body_params, 'Body parameter'), self._generic_generate(context.querystring_params.keys(), context.querystring_params, 'Querystring parameter'), self._generic_generate(HEADER_NAMES.keys(), context.headers, HEADER_NAMES), self._generic_generate(OPTION_NAMES.keys(), context.options, OPTION_NAMES) ) def existing_body_params(self, context, match): params = context.body_params.copy() params.update(context.body_json_params) return self._generic_generate(params.keys(), params, 'Body parameter') def existing_querystring_params(self, context, match): return self._generic_generate( context.querystring_params.keys(), context.querystring_params, 'Querystring parameter') def existing_header_names(self, context, match): return self._generic_generate(context.headers.keys(), context.headers, HEADER_NAMES) def existing_option_names(self, context, match): return self._generic_generate(context.options.keys(), context.options, OPTION_NAMES) def urlpaths(self, context, match): path = urlparse(context.url).path.split('/') overrided_path = match.group(2) if overrided_path: if overrided_path.startswith('/'): # Absolute path path = [] path += overrided_path.split('/')[:-1] names = [ node.name for node in context.root.ls(*path) if node.data.get('type') == 'dir' ] return self._generic_generate(names, {}, 'Endpoint') def _generic_generate(self, names, values, descs): for name in sorted(names): if isinstance(descs, str): desc = descs else: desc = descs.get(name, '') if name in values: value = values[name] if value is None: desc += ' (on)' else: value = str(value) if len(value) > 16: value = value[:13] + '...' desc += ' (=%s)' % value yield name, desc class HttpPromptCompleter(Completer): def __init__(self, context): self.context = context self.comp_gen = CompletionGenerator() def get_completions(self, document, complete_event): cur_text = document.text_before_cursor cur_word = None word_dict = None for regex, method_name in RULES: match = regex.search(cur_text) if match: gen_completions = getattr(self.comp_gen, method_name) completions = gen_completions(self.context, match) word_dict = OrderedDict(completions) groups = match.groups() if len(groups) > 1: cur_word = groups[-1] else: cur_word = document.get_word_before_cursor(WORD=True) break if word_dict: for comp in match_completions(cur_word, word_dict): yield comp ================================================ FILE: http_prompt/completion.py ================================================ """Meta data for autocomplete.""" from collections import OrderedDict from . import options as opt ROOT_COMMANDS = OrderedDict([ ('cd', 'Change URL/path'), ('clear', 'Clear console screen'), ('curl', 'Preview curl command'), ('env', 'Print environment'), ('exec', 'Clear and load environment from a file'), ('exit', 'Exit HTTP Prompt'), ('help', 'List commands, actions, and HTTPie options'), ('httpie', 'Preview HTTPie command'), ('rm *', 'Remove all options and parameters'), ('rm -b', 'Remove body parameter'), ('rm -b *', 'Remove all body parameters'), ('rm -h', 'Remove header'), ('rm -h *', 'Remove all headers'), ('rm -o', 'Remove HTTPie option'), ('rm -o *', 'Remove all HTTPie options'), ('rm -q', 'Remove querystring parameter'), ('rm -q *', 'Remove all querystring parameters'), ('source', 'Load environment from a file'), ]) ACTIONS = OrderedDict([ ('connect', 'CONNECT request'), ('delete', 'DELETE request'), ('get', 'GET request'), ('head', 'HEAD request'), ('options', 'OPTIONS request'), ('patch', 'GET request'), ('post', 'POST request'), ('put', 'PUT request'), ]) # http://www.iana.org/assignments/message-headers/message-headers.xhtml # https://en.wikipedia.org/wiki/List_of_HTTP_header_fields HEADER_NAMES = OrderedDict([ ('Accept', 'Acceptable response media type'), ('Accept-Charset', 'Acceptable response charsets'), ('Accept-Encoding', 'Acceptable response content codings'), ('Accept-Language', 'Preferred natural languages in response'), ('ALPN', 'Application-layer protocol negotiation to use'), ('Alt-Used', 'Alternative host in use'), ('Authorization', 'Authentication information'), ('Cache-Control', 'Directives for caches'), ('Connection', 'Connection options'), ('Content-Encoding', 'Content codings'), ('Content-Language', 'Natural languages for content'), ('Content-Length', 'Anticipated size for payload body'), ('Content-Location', 'Where content was obtained'), ('Content-MD5', 'Base64-encoded MD5 sum of content'), ('Content-Type', 'Content media type'), ('Cookie', 'Stored cookies'), ('Date', 'Datetime when message was originated'), ('Depth', 'Applied only to resource or its members'), ('DNT', 'Do not track user'), ('Expect', 'Expected behaviors supported by server'), ('Forwarded', 'Proxies involved'), ('From', 'Sender email address'), ('Host', 'Target URI'), ('HTTP2-Settings', 'HTTP/2 connection parameters'), ('If', 'Request condition on state tokens and ETags'), ('If-Match', 'Request condition on target resource'), ('If-Modified-Since', 'Request condition on modification date'), ('If-None-Match', 'Request condition on target resource'), ('If-Range', 'Request condition on Range'), ('If-Schedule-Tag-Match', 'Request condition on Schedule-Tag'), ('If-Unmodified-Since', 'Request condition on modification date'), ('Max-Forwards', 'Max number of times forwarded by proxies'), ('MIME-Version', 'Version of MIME protocol'), ('Origin', 'Origin(s) issuing the request'), ('Pragma', 'Implementation-specific directives'), ('Prefer', 'Preferred server behaviors'), ('Proxy-Authorization', 'Proxy authorization credentials'), ('Proxy-Connection', 'Proxy connection options'), ('Range', 'Request transfer of only part of data'), ('Referer', 'Previous web page'), ('TE', 'Transfer codings willing to accept'), ('Transfer-Encoding', 'Transfer codings applied to payload body'), ('Upgrade', 'Invite server to upgrade to another protocol'), ('User-Agent', 'User agent string'), ('Via', 'Intermediate proxies'), ('Warning', 'Possible incorrectness with payload body'), ('WWW-Authenticate', 'Authentication scheme'), ('X-Csrf-Token', 'Prevent cross-site request forgery'), ('X-CSRFToken', 'Prevent cross-site request forgery'), ('X-Forwarded-For', 'Originating client IP address'), ('X-Forwarded-Host', 'Original host requested by client'), ('X-Forwarded-Proto', 'Originating protocol'), ('X-Http-Method-Override', 'Request method override'), ('X-Requested-With', 'Used to identify Ajax requests'), ('X-XSRF-TOKEN', 'Prevent cross-site request forgery'), ]) CONTENT_TYPES = [ 'application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/html', ] # TODO: Include more common header values HEADER_VALUES = { 'Accept': CONTENT_TYPES, 'Content-Type': CONTENT_TYPES, } OPTION_NAMES = sorted(opt.FLAG_OPTIONS + opt.VALUE_OPTIONS) OPTION_NAMES = OrderedDict(OPTION_NAMES) ================================================ FILE: http_prompt/config.py ================================================ """Functions that deal with the user configuration.""" import os import shutil from . import defaultconfig from . import xdg def get_user_config_path(): """Get the path to the user config file.""" return os.path.join(xdg.get_config_dir(), 'config.py') def initialize(): """Initialize a default config file if it doesn't exist yet. Returns: tuple: A tuple of (copied, dst_path). `copied` is a bool indicating if this function created the default config file. `dst_path` is the path of the user config file. """ dst_path = get_user_config_path() copied = False if not os.path.exists(dst_path): src_path = os.path.join(os.path.dirname(__file__), 'defaultconfig.py') shutil.copyfile(src_path, dst_path) copied = True return copied, dst_path def _module_to_dict(module): attrs = {} attr_names = filter(lambda n: not n.startswith('_'), dir(module)) for name in attr_names: value = getattr(module, name) attrs[name] = value return attrs def load_default(): """Return default config as a dict.""" return _module_to_dict(defaultconfig) def load_user(): """Read user config file and return it as a dict.""" config_path = get_user_config_path() config = {} # TODO: This may be overkill and too slow just for reading a config file with open(config_path) as f: code = compile(f.read(), config_path, 'exec') exec(code, config) keys = list(config.keys()) for k in keys: if k.startswith('_'): del config[k] return config def load(): """Read default and user config files and return them as a dict.""" config = load_default() config.update(load_user()) return config ================================================ FILE: http_prompt/context/__init__.py ================================================ from http_prompt.tree import Node class Context(object): def __init__(self, url=None, spec=None): self.url = url self.headers = {} self.querystring_params = {} self.body_params = {} self.body_json_params = {} self.options = {} self.should_exit = False # Create a tree for supporting API spec and ls command self.root = Node('root') if spec: if not self.url: schemes = spec.get('schemes') scheme = schemes[0] if schemes else 'https' self.url = (scheme + '://' + spec.get('host', 'http://localhost:8000') + spec.get('basePath', '')) base_path_tokens = list(filter(lambda s: s, spec.get('basePath', '').split('/'))) paths = spec.get('paths') if paths: for path in paths: path_tokens = (base_path_tokens + list(filter(lambda s: s, path.split('/')))) if path == '/': # Path is a trailing slash path_tokens.insert(len(base_path_tokens), '/') elif path[-1] == '/': # Path ends with a trailing slash path_tokens[-1] = path_tokens[-1] + '/' self.root.add_path(*path_tokens) endpoint = dict(paths[path]) # path parameters (apply to all paths if not overriden) # exclude $ref as we have no system to handle that now global_parameters = list(endpoint.pop('parameters', [])) # not used endpoint.pop('servers', None) endpoint.pop('$ref', None) endpoint.pop('summary', None) endpoint.pop('description', None) for method, info in endpoint.items(): params = info.get('parameters', []) params = list(global_parameters + params) if params: def parameter_key(i): return ( i.get('$ref', None), i.get('name', None), i.get('in', None) ) # parameter is overriden based on $ref/in/name value # last value (local definition) takes precedence params_map = {parameter_key(p): p for p in params} params = params_map.values() for param in params: if param.get('$ref'): for section in param.get('$ref').split('/'): param = param.get( section) if not section == '#' else spec if param.get('in') != 'path': # Note that for completion mechanism, only # name/node_type is used # Parameters from methods/location # are merged full_path = path_tokens + [param['name']] self.root.add_path(*full_path, node_type='file') elif not self.url: self.url = 'http://localhost:8000' def __eq__(self, other): return (self.url == other.url and self.headers == other.headers and self.options == other.options and self.querystring_params == other.querystring_params and self.body_params == other.body_params and self.body_json_params == other.body_json_params and self.should_exit == other.should_exit) def copy(self): context = Context(self.url) context.headers = self.headers.copy() context.querystring_params = self.querystring_params.copy() context.body_params = self.body_params.copy() context.body_json_params = self.body_json_params.copy() context.options = self.options.copy() context.should_exit = self.should_exit return context def update(self, context): if context.url: self.url = context.url self.headers.update(context.headers) self.querystring_params.update(context.querystring_params) self.body_params.update(context.body_params) self.body_json_params.update(context.body_json_params) self.options.update(context.options) self.should_exit = self.should_exit ================================================ FILE: http_prompt/context/transform.py ================================================ """Functions that transform a Context object to a different representation.""" import json from http_prompt.utils import smart_quote def _noop(s): return s def _extract_httpie_options(context, quote=False, join_key_value=False, excluded_keys=None): if quote: quote_func = smart_quote else: quote_func = _noop if join_key_value: def form_new_opts(k, v): return [k + '=' + v] else: def form_new_opts(k, v): return [k, v] excluded_keys = excluded_keys or [] opts = [] for k, v in sorted(context.options.items()): if k not in excluded_keys: if v is not None: v = quote_func(v) new_opts = form_new_opts(k, v) else: new_opts = [k] opts += new_opts return opts def _extract_httpie_request_items(context, quote=False): if quote: quote_func = smart_quote else: quote_func = _noop items = [] operators_and_items = [ # (separator, dict_of_request_items) ('==', context.querystring_params), (':=', context.body_json_params), ('=', context.body_params), (':', context.headers) ] for sep, item_dict in operators_and_items: for k, value in sorted(item_dict.items()): if sep == ':=': json_str = json.dumps(value, sort_keys=True) item = '%s:=%s' % (k, quote_func(json_str)) items.append(item) elif isinstance(value, (list, tuple)): for v in value: item = quote_func('%s%s%s' % (k, sep, v)) items.append(item) else: item = quote_func('%s%s%s' % (k, sep, value)) items.append(item) return items def extract_args_for_httpie_main(context, method=None): """Transform a Context object to a list of arguments that can be passed to HTTPie main function. """ args = _extract_httpie_options(context) if method: args.append(method.upper()) args.append(context.url) args += _extract_httpie_request_items(context) return args def format_to_curl(context, method=None): """Format a Context object to a cURL command.""" raise NotImplementedError('curl format is not supported yet') def format_to_raw(context, method=None): """Format a Context object to HTTP raw text.""" raise NotImplementedError('raw format is not supported yet') def format_to_httpie(context, method=None): """Format a Context object to an HTTPie command.""" cmd = ['http'] + _extract_httpie_options(context, quote=True, join_key_value=True) if method: cmd.append(method.upper()) cmd.append(context.url) cmd += _extract_httpie_request_items(context, quote=True) return ' '.join(cmd) + '\n' def format_to_http_prompt(context, excluded_options=None): """Format a Context object to HTTP Prompt commands.""" cmds = _extract_httpie_options(context, quote=True, join_key_value=True, excluded_keys=excluded_options) cmds.append('cd ' + smart_quote(context.url)) cmds += _extract_httpie_request_items(context, quote=True) return '\n'.join(cmds) + '\n' ================================================ FILE: http_prompt/contextio.py ================================================ """Serialization and deserialization of a Context object.""" import io import os from . import xdg from .context.transform import format_to_http_prompt from .execution import execute # Don't save these HTTPie options to avoid collision with user config file EXCLUDED_OPTIONS = ['--style'] # Filename the current environment context will be saved to CONTEXT_FILENAME = 'context.hp' def _get_context_filepath(): dir_path = xdg.get_data_dir() return os.path.join(dir_path, CONTEXT_FILENAME) def load_context(context, file_path=None): """Load a Context object in place from user data directory.""" if not file_path: file_path = _get_context_filepath() if os.path.exists(file_path): with open(file_path, encoding='utf-8') as f: for line in f: execute(line, context) def save_context(context): """Save a Context object to user data directory.""" file_path = _get_context_filepath() content = format_to_http_prompt(context, excluded_options=EXCLUDED_OPTIONS) with open(file_path, 'w', encoding='utf-8') as f: f.write(content) ================================================ FILE: http_prompt/defaultconfig.py ================================================ # Highlighting style for prompt commands. Available values: # algol, algol_nu, autumn, borland, bw, colorful, default, emacs, friendly, # fruity, igor, lovelace, manni, monokai, murphy, native, paraiso-dark, # paraiso-light, pastie, perldoc, rrt, solarized, tango, trac, vim, vs, xcode. # Preview themes at http://http-prompt.com/themes command_style = 'solarized' # Highlighting style for HTTPie's output. Available values are the same as # command_style. Set this to None to use HTTPie's default style, which you # can refer to https://httpie.org/doc#config-file-location output_style = None # The tool used to paginate output. Available values: 'less' and 'more'. # Note that 'more' does not support ANSI colors. pager = 'less' # What to do when a response has a 'Set-Cookie' header? Available values: # 'auto': set the cookie automatically and silently # 'ask': ask the user if they want to set the cookie # 'off': do nothing with the 'Set-Cookie' header set_cookies = 'auto' # Enable Vi editor mode? Available values: True / False. # When Vi mode is enabled, you use Vi-like keybindings to edit your commands. # When it is disabled, you use Emacs keybindings. vi = False ================================================ FILE: http_prompt/execution.py ================================================ import io import json import re import os import sys from urllib.parse import urlparse, urljoin import click from subprocess import CalledProcessError, Popen, PIPE from httpie.context import Environment from httpie.core import main as httpie_main from parsimonious.exceptions import ParseError, VisitationError from parsimonious.grammar import Grammar from parsimonious.nodes import NodeVisitor from parsimonious.nodes import Node from pygments.token import String, Name from .completion import ROOT_COMMANDS, ACTIONS, OPTION_NAMES, HEADER_NAMES from .context import Context from .context.transform import ( extract_args_for_httpie_main, format_to_curl, format_to_httpie, format_to_http_prompt) from .output import Printer, TextWriter from .utils import unescape, unquote, colformat HTTPIE_PROGRAM_NAME = 'http' grammar = r""" command = mutation / immutation mutation = concat_mut+ / nonconcat_mut immutation = preview / action / ls / env / help / exit / exec / source / clear / _ concat_mut = option_mut / full_quoted_mut / value_quoted_mut / unquoted_mut nonconcat_mut = cd / rm preview = _ tool _ (method _)? (urlpath _)? concat_mut* redir_out? _ action = _ method _ (urlpath _)? concat_mut* redir_out? _ urlpath = (~r"https?://" unquoted_string) / (!concat_mut !redir_out string) clear = _ "clear" _ help = _ "help" _ exit = _ "exit" _ ls = _ "ls" _ (urlpath _)? (redir_out)? env = _ "env" _ (redir_out)? source = _ "source" _ filepath _ exec = _ "exec" _ filepath _ redir_out = redir_append / redir_write / pipe redir_append = _ ">>" _ filepath _ redir_write = _ ">" _ filepath _ pipe = _ "|" _ (shell_subs / shell_code) _ unquoted_mut = _ unquoted_mutkey mutop unquoted_mutval _ full_quoted_mut = full_squoted_mut / full_dquoted_mut value_quoted_mut = value_squoted_mut / value_dquoted_mut full_squoted_mut = _ "'" squoted_mutkey mutop squoted_mutval "'" _ full_dquoted_mut = _ '"' dquoted_mutkey mutop dquoted_mutval '"' _ value_squoted_mut = _ unquoted_mutkey mutop "'" squoted_mutval "'" _ value_dquoted_mut = _ unquoted_mutkey mutop '"' dquoted_mutval '"' _ mutop = ":=" / ":" / "==" / "=" unquoted_mutkey = unquoted_mutkey_item+ unquoted_mutval = unquoted_stringitem* unquoted_mutkey_item = shell_subs / unquoted_mutkey_char / escapeseq unquoted_mutkey_char = ~r"[^\s'\"\\=:>]" squoted_mutkey = squoted_mutkey_item+ squoted_mutval = squoted_stringitem* squoted_mutkey_item = shell_subs / squoted_mutkey_char / escapeseq squoted_mutkey_char = ~r"[^\r\n'\\=:]" dquoted_mutkey = dquoted_mutkey_item+ dquoted_mutval = dquoted_stringitem* dquoted_mutkey_item = shell_subs / dquoted_mutkey_char / escapeseq dquoted_mutkey_char = ~r'[^\r\n"\\=:]' option_mut = flag_option_mut / value_option_mut flag_option_mut = _ flag_optname _ flag_optname = "--json" / "-j" / "--form" / "-f" / "--verbose" / "-v" / "--headers" / "-h" / "--body" / "-b" / "--stream" / "-S" / "--download" / "-d" / "--continue" / "-c" / "--follow" / "--check-status" / "--ignore-stdin" / "--help" / "--version" / "--traceback" / "--debug" value_option_mut = _ value_optname ~r"(\s+|=)" string _ value_optname = "--pretty" / "--style" / "-s" / "--print" / "-p" / "--output" / "-o" / "--session-read-only" / "--session" / "--auth-type" / "--auth" / "-a" / "--proxy" / "--verify" / "--cert" / "--cert-key" / "--timeout" / "--raw" cd = _ "cd" _ string? _ rm = (_ "rm" _ "*" _) / (_ "rm" _ ~r"\-(h|q|b|o)" _ mutkey _) tool = "httpie" / "curl" method = ~r"get"i / ~r"head"i / ~r"post"i / ~r"put"i / ~r"delete"i / ~r"patch"i / ~r"options"i / ~r"connect"i mutkey = unquoted_mutkey / ("'" squoted_mutkey "'") / ('"' dquoted_mutkey '"') / flag_optname / value_optname string = quoted_string / unquoted_string quoted_string = ('"' dquoted_stringitem* '"') / ("'" squoted_stringitem* "'") unquoted_string = unquoted_stringitem+ dquoted_stringitem = shell_subs / dquoted_stringchar / escapeseq squoted_stringitem = shell_subs / squoted_stringchar / escapeseq unquoted_stringitem = shell_subs / unquoted_stringchar / escapeseq dquoted_stringchar = ~r'[^\r\n"\\]' squoted_stringchar = ~r"[^\r\n'\\]" unquoted_stringchar = ~r"[^\s'\\]" escapeseq = ~r"\\." _ = ~r"\s*" shell_subs = "`" shell_code "`" shell_code = ~r"[^`]*" """ if sys.platform == 'win32': # XXX: Windows use backslashes as separators in its filesystem path, so we # have to avoid using backslashes to escape chars here. grammar += r""" filepath = quoted_filepath / unquoted_filepath quoted_filepath = ('"' dquoted_filepath_char+ '"') / ("'" squoted_filepath_char+ "'") dquoted_filepath_char = ~r'[^\r\n"]' squoted_filepath_char = ~r"[^\r\n']" unquoted_filepath = unquoted_filepath_char+ unquoted_filepath_char = ~r"[^\s\"]" """ else: grammar += r""" filepath = string """ grammar = Grammar(grammar) if Environment.colors == 256: from pygments.formatters.terminal256 import ( Terminal256Formatter as TerminalFormatter) else: from pygments.formatters.terminal import TerminalFormatter def urljoin2(base, path, **kwargs): if not base.endswith('/'): base += '/' url = urljoin(base, path, **kwargs) if url.endswith('/') and not path.endswith('/'): url = url[:-1] return url def generate_help_text(): """Return a formatted string listing commands, HTTPie options, and HTTP actions. """ def generate_cmds_with_explanations(summary, cmds): text = '{0}:\n'.format(summary) for cmd, explanation in cmds: text += '\t{0:<10}\t{1:<20}\n'.format(cmd, explanation) return text + '\n' text = generate_cmds_with_explanations('Commands', ROOT_COMMANDS.items()) text += generate_cmds_with_explanations('Options', OPTION_NAMES.items()) text += generate_cmds_with_explanations('Actions', ACTIONS.items()) text += generate_cmds_with_explanations('Headers', HEADER_NAMES.items()) return text if sys.platform == 'win32': # nocover def normalize_filepath(path): return unquote(path) else: def normalize_filepath(path): return unescape(unquote(path)) class DummyExecutionListener(object): def context_changed(self, context): pass def response_returned(self, context, response): pass class ExecutionVisitor(NodeVisitor): unwrapped_exceptions = (CalledProcessError,) def __init__(self, context, listener=None, style=None): super(ExecutionVisitor, self).__init__() self.context = context self.context_override = Context(context.url) self.method = None self.tool = None self._output = Printer() # If there's a pipe, as in "httpie post | sed s/POST/GET/", this # variable points to the "sed" Popen object. The variable is necessary # because the we need to redirect Popen.stdout to Printer, which does # output pagination. self.pipe_proc = None self.listener = listener or DummyExecutionListener() # Last response object returned by HTTPie self.last_response = None # Pygments formatter, used to render output with colors in some cases if style: self.formatter = TerminalFormatter(style=style) else: self.formatter = None @property def output(self): return self._output @output.setter def output(self, new_output): if self._output: self._output.close() self._output = new_output def visit_method(self, node, children): self.method = node.text return node def visit_urlpath(self, node, children): path = node.text self.context_override.url = urljoin2(self.context_override.url, path) return node def visit_cd(self, node, children): _, _, _, path, _ = children if isinstance(path, Node): seg = urlparse(self.context_override.url) self.context_override.url = seg.scheme + '://' + seg.netloc else: self.context_override.url = urljoin2( self.context_override.url, path) return node def visit_rm(self, node, children): children = children[0] kind = children[3].text if kind == '*': # Clear context for target in [self.context.headers, self.context.querystring_params, self.context.body_params, self.context.body_json_params, self.context.options]: target.clear() return node name = children[5] if kind == '-h': target = self.context.headers elif kind == '-q': target = self.context.querystring_params elif kind == '-o': target = self.context.options else: assert kind == '-b' # TODO: This is kind of ugly, will fix it if name == '*': self.context.body_params.clear() self.context.body_json_params.clear() else: try: del self.context.body_params[name] except KeyError: del self.context.body_json_params[name] return node if name == '*': target.clear() else: del target[name] return node def visit_help(self, node, children): self.output.write(generate_help_text()) return node def _redirect_output(self, filepath, mode): filepath = normalize_filepath(filepath) self.output = TextWriter(open(os.path.expandvars(filepath), mode)) def visit_redir_append(self, node, children): self._redirect_output(children[3], 'ab') return node def visit_redir_write(self, node, children): self._redirect_output(children[3], 'wb') return node def visit_pipe(self, node, children): cmd = children[3] self.pipe_proc = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE) self.output = TextWriter(self.pipe_proc.stdin) return node def visit_exec(self, node, children): path = normalize_filepath(children[3]) with open(path, encoding='utf-8') as f: # Wipe out context first execute('rm *', self.context, self.listener) for line in f: execute(line, self.context, self.listener) return node def visit_source(self, node, children): path = normalize_filepath(children[3]) with open(path, encoding='utf-8') as f: for line in f: execute(line, self.context, self.listener) return node def _colorize(self, text, token_type): if not self.formatter: return text out = io.StringIO() self.formatter.format([(token_type, text)], out) return out.getvalue() def visit_ls(self, node, children): path = urlparse(self.context_override.url).path path = filter(None, path.split('/')) nodes = self.context.root.ls(*path) if self.output.isatty(): names = [] for node in nodes: token_type = String if node.data.get('type') == 'dir' else Name name = self._colorize(node.name, token_type) names.append(name) lines = list(colformat(list(names))) else: lines = [n.name for n in nodes] if lines: self.output.write('\n'.join(lines)) return node def visit_env(self, node, children): text = format_to_http_prompt(self.context) self.output.write(text) return node def visit_exit(self, node, children): self.context.should_exit = True return node def visit_clear(self, node, children): self.output.clear() return node def visit_mutkey(self, node, children): if isinstance(children[0], list): return children[0][1] return children[0] def _mutate(self, node, key, op, val): if op == ':=': self.context_override.body_json_params[key] = json.loads(val) elif op == ':': self.context_override.headers[key] = val elif op == '=': self.context_override.body_params[key] = val elif op == '==': # You can have multiple querystring params with the same name, # so we use a list to store multiple values (#20) params = self.context_override.querystring_params if key not in params: params[key] = [val] else: params[key].append(val) return node def visit_unquoted_mut(self, node, children): _, key, op, val, _ = children return self._mutate(node, key, op, val) def visit_full_squoted_mut(self, node, children): _, _, key, op, val, _, _ = children return self._mutate(node, key, op, val) def visit_full_dquoted_mut(self, node, children): _, _, key, op, val, _, _ = children return self._mutate(node, key, op, val) def visit_value_squoted_mut(self, node, children): _, key, op, _, val, _, _ = children return self._mutate(node, key, op, val) def visit_value_dquoted_mut(self, node, children): _, key, op, _, val, _, _ = children return self._mutate(node, key, op, val) def _visit_mut_key_or_val(self, node, children): return unescape(''.join(children), exclude=':=') visit_unquoted_mutkey = _visit_mut_key_or_val visit_unquoted_mutval = _visit_mut_key_or_val visit_squoted_mutkey = _visit_mut_key_or_val visit_squoted_mutval = _visit_mut_key_or_val visit_dquoted_mutkey = _visit_mut_key_or_val visit_dquoted_mutval = _visit_mut_key_or_val def visit_mutop(self, node, children): return node.text def visit_flag_option_mut(self, node, children): _, key, _ = children self.context_override.options[key] = None return node def visit_flag_optname(self, node, children): return node.text def visit_value_option_mut(self, node, children): _, key, _, val, _ = children self.context_override.options[key] = val return node def visit_value_optname(self, node, children): return node.text def visit_filepath(self, node, children): return children[0] def visit_string(self, node, children): return children[0] def visit_quoted_filepath(self, node, children): return node.text[1:-1] def visit_unquoted_filepath(self, node, children): return node.text def visit_unquoted_string(self, node, children): return unescape(''.join(children)) def visit_quoted_string(self, node, children): return self._visit_mut_key_or_val(node, children[0][1]) def _visit_stringitem(self, node, children): child = children[0] if hasattr(child, 'text'): return child.text return child visit_unquoted_mutkey_item = _visit_stringitem visit_unquoted_stringitem = _visit_stringitem visit_squoted_mutkey_item = _visit_stringitem visit_squoted_stringitem = _visit_stringitem visit_dquoted_mutkey_item = _visit_stringitem visit_dquoted_stringitem = _visit_stringitem def visit_tool(self, node, children): self.tool = node.text return node def visit_mutation(self, node, children): self.context.update(self.context_override) self.listener.context_changed(self.context) return node def _final_context(self): context = self.context.copy() context.update(self.context_override) return context def _trace_get_response(self, frame, event, arg): func_name = frame.f_code.co_name if func_name == 'get_response': if event == 'call': return self._trace_get_response elif event == 'return': self.last_response = arg def _call_httpie_main(self): context = self._final_context() args = extract_args_for_httpie_main(context, self.method) env = Environment(stdout=self.output, stdin=sys.stdin, is_windows=False) env.stdout_isatty = self.output.isatty() env.stdin_isatty = sys.stdin.isatty() # XXX: httpie_main() doesn't provide an API for us to get the # HTTP response object, so we use this super dirty hack - # sys.settrace() to intercept get_response() that is called in # httpie_main() internally. The HTTP response intercepted is # assigned to self.last_response, which self.listener may be # interested in. sys.settrace(self._trace_get_response) try: httpie_main([HTTPIE_PROGRAM_NAME, *args], env=env) finally: sys.settrace(None) def visit_immutation(self, node, children): self.output.close() if self.pipe_proc: Printer().write(self.pipe_proc.stdout.read()) return node def visit_preview(self, node, children): context = self._final_context() if self.tool == 'httpie': command = format_to_httpie(context, self.method) else: assert self.tool == 'curl' command = format_to_curl(context, self.method) self.output.write(command) return node def visit_action(self, node, children): self._call_httpie_main() if self.last_response: self.listener.response_returned(self.context, self.last_response) return node def visit_shell_subs(self, node, children): cmd = children[1] p = Popen(cmd, shell=True, stdout=PIPE) return p.stdout.read().decode().rstrip() def visit_shell_code(self, node, children): return node.text def generic_visit(self, node, children): if not node.expr_name and node.children: if len(children) == 1: return children[0] return children return node def execute(command, context, listener=None, style=None): try: root = grammar.parse(command) except ParseError as err: # TODO: Better error message part = command[err.pos:err.pos + 10] click.secho('Syntax error near "%s"' % part, err=True, fg='red') else: visitor = ExecutionVisitor(context, listener=listener, style=style) try: visitor.visit(root) except VisitationError as err: exc_class = err.original_class if exc_class is KeyError: # XXX: Need to parse VisitationError error message to get the # original error message as VisitationError doesn't hold the # original exception object key = re.search(r"KeyError: u?'(.*)'", str(err)).group(1) click.secho("Key '%s' not found" % key, err=True, fg='red') elif issubclass(exc_class, OSError): msg = str(err).splitlines()[0] # Remove the exception class name at the beginning msg = msg[msg.find(':') + 2:] click.secho(msg, err=True, fg='red') else: # TODO: Better error message click.secho(str(err), err=True, fg='red') except CalledProcessError as err: click.secho(err.output + ' (exit status %d)' % err.returncode, fg='red') ================================================ FILE: http_prompt/lexer.py ================================================ from pygments.lexer import (RegexLexer, bygroups, words, using, include, combined) from pygments.lexers import BashLexer from pygments.token import Text, String, Keyword, Name, Operator from . import options as opt __all__ = ['HttpPromptLexer'] FLAG_OPTIONS = [name for name, _ in opt.FLAG_OPTIONS] VALUE_OPTIONS = [name for name, _ in opt.VALUE_OPTIONS] HTTP_METHODS = ('get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'connect') def string_rules(state): return [ (r'(")((?:[^\r\n"\\]|(?:\\.))+)(")', bygroups(Text, String, Text), state), (r'(")((?:[^\r\n"\\]|(?:\\.))+)', bygroups(Text, String), state), (r"(')((?:[^\r\n'\\]|(?:\\.))+)(')", bygroups(Text, String, Text), state), (r"(')((?:[^\r\n'\\]|(?:\\.))+)", bygroups(Text, String), state), (r'([^\s\'\\]|(\\.))+', String, state) ] class HttpPromptLexer(RegexLexer): name = 'HttpPrompt' aliases = ['http-prompt'] filenames = ['*.http-prompt'] tokens = { 'root': [ (r'\s+', Text), (r'(cd)(\s*)', bygroups(Keyword, Text), 'cd'), (r'(rm)(\s*)', bygroups(Keyword, Text), 'rm_option'), (r'(httpie|curl)(\s*)', bygroups(Keyword, Text), 'action'), (words(HTTP_METHODS, prefix='(?i)', suffix=r'(?!\S)(\s*)'), bygroups(Keyword, Text), combined('redir_out', 'urlpath')), (r'(clear)(\s*)', bygroups(Keyword, Text), 'end'), (r'(exit)(\s*)', bygroups(Keyword, Text), 'end'), (r'(help)(\s)*', bygroups(Keyword, Text), 'end'), (r'(env)(\s*)', bygroups(Keyword, Text), combined('redir_out', 'pipe')), (r'(source)(\s*)', bygroups(Keyword, Text), 'file_path'), (r'(exec)(\s*)', bygroups(Keyword, Text), 'file_path'), (r'(ls)(\s*)', bygroups(Keyword, Text), combined('redir_out', 'urlpath')), (r'', Text, 'concat_mut') ], 'cd': string_rules('end'), 'rm_option': [ (r'(\-(?:h|o|b|q))(\s*)', bygroups(Name, Text), 'rm_name'), (r'(\*)(\s*)', bygroups(Name, Text), 'end') ], 'rm_name': string_rules('end'), 'shell_command': [ (r'(`)([^`]*)(`)', bygroups(Text, using(BashLexer), Text)), ], 'pipe': [ (r'(\s*)(\|)(.*)', bygroups(Text, Operator, using(BashLexer))), ], 'concat_mut': [ (r'$', Text, 'end'), (r'\s+', Text), # Flag options, such as (--form) and (--json) (words(FLAG_OPTIONS, suffix=r'\b'), Name, 'concat_mut'), # Options with values, such as (--style=default) and (--pretty all) (words(VALUE_OPTIONS, suffix=r'\b'), Name, combined('shell_command', 'option_op')), include('shell_command'), # Unquoted or value-quoted request mutation, # such as (name="John Doe") and (name=John\ Doe) (r'((?:[^\s\'"\\=:]|(?:\\.))*)(:=|:|==|=)', bygroups(Name, Operator), combined('shell_command', 'unquoted_mut')), # Full single-quoted request mutation, such as ('name=John Doe') (r"(')((?:[^\r\n'\\=:]|(?:\\.))+)(:=|:|==|=)", bygroups(Text, Name, Operator), combined('shell_command', 'squoted_mut')), # Full double-quoted request mutation, such as ("name=John Doe") (r'(")((?:[^\r\n"\\=:]|(?:\\.))+)(:=|:|==|=)', bygroups(Text, Name, Operator), combined('shell_command', 'dquoted_mut')) ], 'option_op': [ (r'(\s+|=)', Operator, 'option_value'), ], 'option_value': string_rules('#pop:2'), 'file_path': string_rules('end'), 'redir_out': [ (r'(?i)(>>?)(\s*)', bygroups(Operator, Text), 'file_path') ], 'unquoted_mut': string_rules('#pop'), 'squoted_mut': [ (r"((?:[^\r\n'\\]|(?:\\.))+)(')", bygroups(String, Text), '#pop'), (r"([^\r\n'\\]|(\\.))+", String, '#pop') ], 'dquoted_mut': [ (r'((?:[^\r\n"\\]|(?:\\.))+)(")', bygroups(String, Text), '#pop'), (r'([^\r\n"\\]|(\\.))+', String, '#pop') ], 'action': [ (words(HTTP_METHODS, prefix='(?i)', suffix=r'(\s*)'), bygroups(Keyword, Text), combined('redir_out', 'pipe', 'urlpath')), (r'', Text, combined('redir_out', 'pipe', 'urlpath')) ], 'urlpath': [ (r'https?://([^\s"\'\\]|(\\.))+', String, combined('concat_mut', 'redir_out', 'pipe')), (r'(")(https?://(?:[^\r\n"\\]|(?:\\.))+)(")', bygroups(Text, String, Text), combined('concat_mut', 'redir_out', 'pipe')), (r'(")(https?://(?:[^\r\n"\\]|(?:\\.))+)', bygroups(Text, String)), (r"(')(https?://(?:[^\r\n'\\]|(?:\\.))+)(')", bygroups(Text, String, Text), combined('concat_mut', 'redir_out', 'pipe')), (r"(')(https?://(?:[^\r\n'\\]|(?:\\.))+)", bygroups(Text, String)), (r'(")((?:[^\r\n"\\=:]|(?:\\.))+)(")', bygroups(Text, String, Text), combined('concat_mut', 'redir_out', 'pipe')), (r'(")((?:[^\r\n"\\=:]|(?:\\.))+)', bygroups(Text, String)), (r"(')((?:[^\r\n'\\=:]|(?:\\.))+)(')", bygroups(Text, String, Text), combined('concat_mut', 'redir_out', 'pipe')), (r"(')((?:[^\r\n'\\=:]|(?:\\.))+)", bygroups(Text, String)), (r'([^\-](?:[^\s"\'\\=:]|(?:\\.))+)(\s+|$)', bygroups(String, Text), combined('concat_mut', 'redir_out', 'pipe')), (r'', Text, combined('concat_mut', 'redir_out', 'pipe')) ], 'end': [ (r'\n', Text, 'root') ] } ================================================ FILE: http_prompt/options.py ================================================ """Meta data for HTTPie options.""" FLAG_OPTIONS = [ ('--body', 'Print only response body'), ('--check-status', 'Check HTTP status code'), ('--continue', 'Resume an interrupted download'), ('--debug', 'Print debug information'), ('--download', 'Download as a file'), ('--follow', 'Allow full redirects'), ('--form', 'Send as form fields'), ('--headers', 'Print only response headers'), ('--help', 'Show tool (HTTPie, cURL) help message'), ('--ignore-stdin', 'Do not read stdin'), ('--json', 'Send as a JSON object (default)'), ('--stream', 'Stream the output'), ('--traceback', 'Print exception traceback'), ('--verbose', 'Print the whole request and response'), ('--version', 'Show version'), ('-b', 'Shorthand for --body'), ('-c', 'Shorthand for --continue'), ('-d', 'Shorthand for --download'), ('-f', 'Shorthand for --form'), ('-h', 'Shorthand for --headers'), ('-j', 'Shorthand for --json'), ('-S', 'Shorthand for --stream'), ('-v', 'Shorthand for --verbose'), ] VALUE_OPTIONS = [ ('--auth', 'Do authentication'), ('--auth-type', 'Authentication mechanism to be used'), ('--cert', 'Specify client SSL certificate'), ('--cert-key', 'The private key to use with SSL'), ('--output', 'Save output to a file'), ('--pretty', 'Control output processing'), ('--print', 'Specify what output should contain'), ('--proxy', 'Specify proxy URL'), ('--raw', 'Pass raw request data without extra processing'), ('--session', 'Create, or reuse and update a session'), ('--session-read-only', 'Create or read a session'), ('--style', 'Output coloring style'), ('--timeout', 'Connection timeout in seconds'), ('--verify', 'Set to "no" to skip SSL certificate checking'), ('-a', 'Shorthand for --auth'), ('-o', 'Shorthand for --output'), ('-p', 'Shorthand for --print'), ('-s', 'Shorthand for --style'), ] PRETTY_CHOICES = ('all', 'colors', 'format', 'none') STYLE_CHOICES = ('algol', 'algol_nu', 'autumn', 'borland', 'bw', 'colorful', 'default', 'emacs', 'friendly', 'fruity', 'igor', 'lovelace', 'manni', 'monokai', 'murphy', 'native', 'paraiso-dark', 'paraiso-light', 'pastie', 'perldoc', 'rrt', 'solarized', 'tango', 'trac', 'vim', 'vs', 'xcode') AUTH_TYPE_CHOICES = ('basic', 'digest') VERIFY_CHOICES = ('no', 'yes') OPTION_VALUE_CHOICES = { '--auth-type': AUTH_TYPE_CHOICES, '--pretty': PRETTY_CHOICES, '--style': STYLE_CHOICES, '--verify': VERIFY_CHOICES, '-p': PRETTY_CHOICES, '-s': STYLE_CHOICES, } ================================================ FILE: http_prompt/output.py ================================================ import sys import click class Printer(object): """Wrap click.echo_via_pager() so it accepts binary data.""" def write(self, data): if isinstance(data, bytes): data = data.decode() # echo_via_pager() already appends a '\n' at the end of text, # so we use rstrip() to remove extra newlines (#89) click.echo_via_pager(data.rstrip()) def flush(self): pass def close(self): pass def isatty(self): return True def fileno(self): return sys.stdout.fileno() def clear(self): click.clear() class TextWriter(object): """Wrap a file-like object, opened with 'wb' or 'ab', so it accepts text data. """ def __init__(self, fp): self.fp = fp def write(self, data): if isinstance(data, str): data = data.encode() self.fp.write(data) def flush(self): self.fp.flush() def close(self): self.fp.close() def isatty(self): return self.fp.isatty() def fileno(self): return self.fp.fileno() ================================================ FILE: http_prompt/tree.py ================================================ """Tree data structure for ls command to work with OpenAPI specification.""" class Node(object): def __init__(self, name, data=None, parent=None): if name in ('.', '..'): raise ValueError("name cannot be '.' or '..'") self.name = name self.data = data or {} self.parent = parent self.children = set() def __str__(self): return self.name def __repr__(self): return "Node('{}', '{}')".format(self.name, self.data.get('type')) def __lt__(self, other): ta = self.data.get('type') tb = other.data.get('type') if ta != tb: return ta < tb return self.name < other.name def __eq__(self, other): return self.name == other.name and self.data == other.data def __hash__(self): return hash((self.name, self.data.get('type'))) def add_path(self, *path, **kwargs): node_type = kwargs.get('node_type', 'dir') name = path[0] tail = path[1:] child = self.find_child(name, wildcard=False) if not child: data = {'type': 'dir' if tail else node_type} child = Node(name, data=data, parent=self) self.children.add(child) if tail: child.add_path(*tail, node_type=node_type) def find_child(self, name, wildcard=True): for child in self.children: if child.name == name: return child # Attempt to match wildcard like /users/{user_id} if wildcard: for child in self.children: if child.name.startswith('{') and child.name.endswith('}'): return child return None def ls(self, *path): success = True cur = self for name in path: if not name or name == '.': continue elif name == '..': if cur.parent: cur = cur.parent else: child = cur.find_child(name) if child: cur = child else: success = False break if success: for node in sorted(cur.children): yield node ================================================ FILE: http_prompt/utils.py ================================================ import math import re import shlex from prompt_toolkit.output.defaults import create_output RE_ANSI_ESCAPE = re.compile(r'\x1b[^m]*m') def smart_quote(s): return shlex.quote(s) def unquote(s): quotes = ["'", '"'] quote_str = None if s[0] in quotes: quote_str = s[0] if quote_str and s[-1] == quote_str: return s[1: -1] return s def unescape(s, exclude=None): if exclude: char = '[^%s]' % exclude else: char = '.' return re.sub(r'\\(%s)' % char, r'\1', s) def get_terminal_size(): return create_output().get_size() def strip_ansi_escapes(text): return RE_ANSI_ESCAPE.sub('', text) def colformat(strings, num_sep_spaces=1, terminal_width=None): """Format a list of strings like ls does multi-column output.""" if terminal_width is None: terminal_width = get_terminal_size().columns if not strings: return num_items = len(strings) max_len = max([len(strip_ansi_escapes(s)) for s in strings]) num_columns = min( int((terminal_width + num_sep_spaces) / (max_len + num_sep_spaces)), num_items) num_columns = max(1, num_columns) num_lines = int(math.ceil(float(num_items) / num_columns)) num_columns = int(math.ceil(float(num_items) / num_lines)) num_elements_last_column = num_items % num_lines if num_elements_last_column == 0: num_elements_last_column = num_lines lines = [] for i in range(num_lines): line_size = num_columns if i >= num_elements_last_column: line_size -= 1 lines.append([None] * line_size) for i, line in enumerate(lines): line_size = len(line) for j in range(line_size): k = i + num_lines * j item = strings[k] if j % line_size != line_size - 1: item_len = len(strip_ansi_escapes(item)) item = item + ' ' * (max_len - item_len) line[j] = item sep = ' ' * num_sep_spaces for line in lines: yield sep.join(line) ================================================ FILE: http_prompt/xdg.py ================================================ """XDG Base Directory Specification. See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html https://github.com/ActiveState/appdirs """ import os import sys from functools import partial def _get_dir(envvar_name, default_dir, resource_name=None): base_dir = os.getenv(envvar_name) or default_dir app_dir = os.path.join(base_dir, 'http-prompt') if not os.path.exists(app_dir): os.makedirs(app_dir, mode=0o700) if resource_name: app_dir = os.path.join(app_dir, resource_name) if not os.path.exists(app_dir): os.mkdir(app_dir) return app_dir if sys.platform == 'win32': # nocover # NOTE: LOCALAPPDATA is not available on Windows XP get_data_dir = partial(_get_dir, 'LOCALAPPDATA', os.path.expanduser('~/AppData/Local')) get_config_dir = partial(_get_dir, 'LOCALAPPDATA', os.path.expanduser('~/AppData/Local')) else: get_data_dir = partial(_get_dir, 'XDG_DATA_HOME', os.path.expanduser('~/.local/share')) get_config_dir = partial(_get_dir, 'XDG_CONFIG_HOME', os.path.expanduser('~/.config')) ================================================ FILE: requirements-test.txt ================================================ pexpect>=4.2.1 pytest>=3.0.6 pytest-cov>=2.4.0 wheel twine ================================================ FILE: requirements.txt ================================================ click>=5.0 httpie>=2.5.0 parsimonious>=0.6.2 prompt-toolkit>=2.0.0,<3.0.0 Pygments>=2.1.0 PyYAML>=3.0 ================================================ FILE: setup.cfg ================================================ [wheel] universal = 1 ================================================ FILE: setup.py ================================================ import os import re from setuptools import setup here = os.path.abspath(os.path.dirname(__file__)) # Read the version number from a source file. # Why read it, and not import? # see https://groups.google.com/d/topic/pypa-dev/0PkjVpcxTzQ/discussion def find_version(*file_paths): # Open in Latin-1 so that we avoid encoding errors. with open(os.path.join(here, *file_paths), encoding='latin1') as f: version_file = f.read() # The version line must have the form # __version__ = 'ver' version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError('Unable to find version string') def read_description(filename): with open(filename, encoding='utf-8') as f: return f.read() def read_requirements(filename): try: with open(filename) as f: return [line.rstrip() for line in f] except OSError: raise OSError(os.getcwd()) setup( name='http-prompt', version=find_version('http_prompt', '__init__.py'), url='https://github.com/httpie/http-prompt', description='An interactive HTTP command-line client', long_description=read_description('README.rst'), author='Chang-Hung Liang', author_email='eliang.cs@gmail.com', license='MIT', packages=['http_prompt', 'http_prompt.context'], entry_points=""" [console_scripts] http-prompt=http_prompt.cli:cli """, install_requires=read_requirements('requirements.txt'), classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: MIT License', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development', 'Topic :: System :: Networking', 'Topic :: Terminals', 'Topic :: Text Processing', 'Topic :: Utilities', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ] ) ================================================ FILE: snap/snapcraft.yaml ================================================ name: http-prompt summary: Interactive command-line HTTP client description: | HTTP Prompt is an interactive command-line HTTP client featuring autocomplete and syntax highlighting, built on HTTPie and prompt_toolkit. Home: http://http-prompt.com adopt-info: http-prompt confinement: strict apps: http-prompt: command: bin/http-prompt plugs: [network] parts: http-prompt: source: . plugin: python override-pull: | snapcraftctl pull version="$(git describe --always | sed -e 's/-/+git/;y/-/./')" case $version in v*) version=$(echo $version | tail -c +2) ;; *) version=$(echo $version | head -c 32) ;; esac [ -n "$(echo $version | grep "+git")" ] && grade=devel || grade=stable snapcraftctl set-version "$version" snapcraftctl set-grade "$grade" ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/base.py ================================================ import os import shutil import sys import tempfile import unittest class TempAppDirTestCase(unittest.TestCase): """Set up temporary app data and config directories before every test method, and delete them afterwards. """ def setUp(self): # Create a temp dir that will contain data and config directories self.temp_dir = tempfile.mkdtemp() if sys.platform == 'win32': self.homes = { # subdir_name: envvar_name 'data': 'LOCALAPPDATA', 'config': 'LOCALAPPDATA' } else: self.homes = { # subdir_name: envvar_name 'data': 'XDG_DATA_HOME', 'config': 'XDG_CONFIG_HOME' } # Used to restore self.orig_envvars = {} for subdir_name, envvar_name in self.homes.items(): if envvar_name in os.environ: self.orig_envvars[envvar_name] = os.environ[envvar_name] os.environ[envvar_name] = os.path.join(self.temp_dir, subdir_name) def tearDown(self): # Restore envvar values for name in self.homes.values(): if name in self.orig_envvars: os.environ[name] = self.orig_envvars[name] else: del os.environ[name] shutil.rmtree(self.temp_dir) def make_tempfile(self, data='', subdir_name=''): """Create a file under self.temp_dir and return the path.""" full_tempdir = os.path.join(self.temp_dir, subdir_name) if not os.path.exists(full_tempdir): os.makedirs(full_tempdir) if isinstance(data, str): data = data.encode() with tempfile.NamedTemporaryFile(dir=full_tempdir, delete=False) as f: f.write(data) return f.name ================================================ FILE: tests/context/test_context.py ================================================ from http_prompt.context import Context def test_creation(): context = Context('http://example.com') assert context.url == 'http://example.com' assert context.options == {} assert context.headers == {} assert context.querystring_params == {} assert context.body_params == {} assert not context.should_exit def test_creation_with_longer_url(): context = Context('http://example.com/a/b/c/index.html') assert context.url == 'http://example.com/a/b/c/index.html' assert context.options == {} assert context.headers == {} assert context.querystring_params == {} assert context.body_params == {} assert not context.should_exit def test_eq(): c1 = Context('http://localhost') c2 = Context('http://localhost') assert c1 == c2 c1.options['--verify'] = 'no' assert c1 != c2 def test_copy(): c1 = Context('http://localhost') c2 = c1.copy() assert c1 == c2 assert c1 is not c2 def test_update(): c1 = Context('http://localhost') c1.headers['Accept'] = 'application/json' c1.querystring_params['flag'] = '1' c1.body_params.update({ 'name': 'John Doe', 'email': 'john@example.com' }) c2 = Context('http://example.com') c2.headers['Content-Type'] = 'text/html' c2.body_params['name'] = 'John Smith' c1.update(c2) assert c1.url == 'http://example.com' assert c1.headers == { 'Accept': 'application/json', 'Content-Type': 'text/html' } assert c1.querystring_params == {'flag': '1'} assert c1.body_params == { 'name': 'John Smith', 'email': 'john@example.com' } def test_spec(): c = Context('http://localhost', spec={ 'paths': { '/users': { 'get': { 'parameters': [ {'name': 'username', 'in': 'path'}, {'name': 'since', 'in': 'query'}, {'name': 'Accept'} ] } }, '/orgs/{org}': { 'get': { 'parameters': [ {'name': 'org', 'in': 'path'}, {'name': 'featured', 'in': 'query'}, {'name': 'X-Foo', 'in': 'header'} ] } } } }) assert c.url == 'http://localhost' root_children = list(sorted(c.root.children)) assert len(root_children) == 2 assert root_children[0].name == 'orgs' assert root_children[1].name == 'users' orgs_children = list(sorted(root_children[0].children)) assert len(orgs_children) == 1 org_children = list(sorted(list(orgs_children)[0].children)) assert len(org_children) == 2 assert org_children[0].name == 'X-Foo' assert org_children[1].name == 'featured' users_children = list(sorted(root_children[1].children)) assert len(users_children) == 2 assert users_children[0].name == 'Accept' assert users_children[1].name == 'since' def test_override(): """Parameters can be defined at path level """ c = Context('http://localhost', spec={ 'paths': { '/users': { 'parameters': [ {'name': 'username', 'in': 'query'}, {'name': 'Accept', 'in': 'header'} ], 'get': { 'parameters': [ {'name': 'custom1', 'in': 'query'} ] }, 'post': { 'parameters': [ {'name': 'custom2', 'in': 'query'}, ] }, }, '/orgs': { 'parameters': [ {'name': 'username', 'in': 'query'}, {'name': 'Accept', 'in': 'header'} ], 'get': {} } } }) assert c.url == 'http://localhost' root_children = list(sorted(c.root.children)) # one path assert len(root_children) == 2 assert root_children[0].name == 'orgs' assert root_children[1].name == 'users' orgs_methods = list(sorted(list(root_children)[0].children)) # path parameters are used even if no method parameter assert len(orgs_methods) == 2 assert next(filter(lambda i:i.name == 'username', orgs_methods), None) is not None assert next(filter(lambda i:i.name == 'Accept', orgs_methods), None) is not None users_methods = list(sorted(list(root_children)[1].children)) # path and methods parameters are merged assert len(users_methods) == 4 assert next(filter(lambda i:i.name == 'username', users_methods), None) is not None assert next(filter(lambda i:i.name == 'custom1', users_methods), None) is not None assert next(filter(lambda i:i.name == 'custom2', users_methods), None) is not None assert next(filter(lambda i:i.name == 'Accept', users_methods), None) is not None ================================================ FILE: tests/context/test_transform.py ================================================ from http_prompt.context import Context from http_prompt.context import transform as t def test_extract_args_for_httpie_main_get(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.querystring_params.update({ 'page': '2', 'limit': '10' }) args = t.extract_args_for_httpie_main(c, method='get') assert args == ['GET', 'http://localhost/things', 'limit==10', 'page==2', 'Accept:text/html', 'Authorization:ApiKey 1234'] def test_extract_args_for_httpie_main_post(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.options.update({ '--verify': 'no', '--form': None }) c.body_params.update({ 'full name': 'Jane Doe', 'email': 'jane@example.com' }) args = t.extract_args_for_httpie_main(c, method='post') assert args == ['--form', '--verify', 'no', 'POST', 'http://localhost/things', 'email=jane@example.com', 'full name=Jane Doe', 'Accept:text/html', 'Authorization:ApiKey 1234'] def test_extract_raw_json_args_for_httpie_main_post(): c = Context('http://localhost/things') c.body_json_params.update({ 'enabled': True, 'items': ['foo', 'bar'], 'object': { 'id': 10, 'name': 'test' } }) args = t.extract_args_for_httpie_main(c, method='post') assert args == ['POST', 'http://localhost/things', 'enabled:=true', 'items:=["foo", "bar"]', 'object:={"id": 10, "name": "test"}'] def test_format_to_httpie_get(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.querystring_params.update({ 'page': '2', 'limit': '10', 'name': ['alice', 'bob bob'] }) output = t.format_to_httpie(c, method='get') assert output == ("http GET http://localhost/things " "limit==10 name==alice 'name==bob bob' page==2 " "Accept:text/html 'Authorization:ApiKey 1234'\n") def test_format_to_httpie_post(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.options.update({ '--verify': 'no', '--form': None }) c.body_params.update({ 'full name': 'Jane Doe', 'email': 'jane@example.com' }) output = t.format_to_httpie(c, method='post') assert output == ("http --form --verify=no POST http://localhost/things " "email=jane@example.com 'full name=Jane Doe' " "Accept:text/html 'Authorization:ApiKey 1234'\n") def test_format_to_http_prompt_1(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.querystring_params.update({ 'page': '2', 'limit': '10' }) output = t.format_to_http_prompt(c) assert output == ("cd http://localhost/things\n" "limit==10\n" "page==2\n" "Accept:text/html\n" "'Authorization:ApiKey 1234'\n") def test_format_to_http_prompt_2(): c = Context('http://localhost/things') c.headers.update({ 'Authorization': 'ApiKey 1234', 'Accept': 'text/html' }) c.options.update({ '--verify': 'no', '--form': None }) c.body_params.update({ 'full name': 'Jane Doe', 'email': 'jane@example.com' }) output = t.format_to_http_prompt(c) assert output == ("--form\n" "--verify=no\n" "cd http://localhost/things\n" "email=jane@example.com\n" "'full name=Jane Doe'\n" "Accept:text/html\n" "'Authorization:ApiKey 1234'\n") def test_format_raw_json_string_to_http_prompt(): c = Context('http://localhost/things') c.body_json_params.update({ 'bar': 'baz', }) output = t.format_to_http_prompt(c) assert output == ("cd http://localhost/things\n" "bar:='\"baz\"'\n") def test_extract_httpie_options(): c = Context('http://localhost') c.options.update({ '--verify': 'no', '--form': None }) output = t._extract_httpie_options(c, excluded_keys=['--form']) assert output == ['--verify', 'no'] ================================================ FILE: tests/test_cli.py ================================================ import json import os import sys import unittest from unittest.mock import patch, DEFAULT from click.testing import CliRunner from requests.models import Response from .base import TempAppDirTestCase from http_prompt import xdg from http_prompt.context import Context from http_prompt.cli import cli, execute, ExecutionListener def run_and_exit(cli_args=None, prompt_commands=None): """Run http-prompt executable, execute some prompt commands, and exit.""" if cli_args is None: cli_args = [] # Make sure last command is 'exit' if prompt_commands is None: prompt_commands = ['exit'] else: prompt_commands += ['exit'] # Fool cli() so that it believes we're running from CLI instead of pytest. # We will restore it at the end of the function. orig_argv = sys.argv sys.argv = ['http-prompt'] + cli_args try: with patch.multiple('http_prompt.cli', prompt=DEFAULT, execute=DEFAULT) as mocks: mocks['execute'].side_effect = execute # prompt() is mocked to return the command in 'prompt_commands' in # sequence, i.e., prompt() returns prompt_commands[i-1] when it is # called for the ith time mocks['prompt'].side_effect = prompt_commands result = CliRunner().invoke(cli, cli_args) context = mocks['execute'].call_args[0][1] return result, context finally: sys.argv = orig_argv class TestCli(TempAppDirTestCase): def test_without_args(self): result, context = run_and_exit(['http://localhost']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://localhost') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {}) def test_incomplete_url1(self): result, context = run_and_exit(['://example.com']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {}) def test_incomplete_url2(self): result, context = run_and_exit(['//example.com']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {}) def test_incomplete_url3(self): result, context = run_and_exit(['example.com']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {}) def test_httpie_oprions(self): url = 'http://example.com' custom_args = '--auth value: name=foo' result, context = run_and_exit([url] + custom_args.split()) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {'--auth': 'value:'}) self.assertEqual(context.body_params, {'name': 'foo'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {}) def test_persistent_context(self): result, context = run_and_exit(['//example.com', 'name=bob', 'id==10']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'bob'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) result, context = run_and_exit() self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'bob'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) def test_cli_args_bypasses_persistent_context(self): result, context = run_and_exit(['//example.com', 'name=bob', 'id==10']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'bob'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) result, context = run_and_exit(['//example.com', 'sex=M']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'sex': 'M'}) self.assertEqual(context.headers, {}) def test_config_file(self): # Config file is not there at the beginning config_path = os.path.join(xdg.get_config_dir(), 'config.py') self.assertFalse(os.path.exists(config_path)) # After user runs it for the first time, a default config file should # be created result, context = run_and_exit(['//example.com']) self.assertEqual(result.exit_code, 0) self.assertTrue(os.path.exists(config_path)) def test_cli_arguments_with_spaces(self): result, context = run_and_exit(['example.com', "name=John Doe", "Authorization:Bearer API KEY"]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.querystring_params, {}) self.assertEqual(context.body_params, {'name': 'John Doe'}) self.assertEqual(context.headers, {'Authorization': 'Bearer API KEY'}) def test_spec_from_local(self): spec_filepath = self.make_tempfile(json.dumps({ 'paths': { '/users': {}, '/orgs': {} } })) result, context = run_and_exit(['example.com', "--spec", spec_filepath]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(set([n.name for n in context.root.children]), set(['users', 'orgs'])) def test_spec_basePath(self): spec_filepath = self.make_tempfile(json.dumps({ 'basePath': '/api/v1', 'paths': { '/users': {}, '/orgs': {} } })) result, context = run_and_exit(['example.com', "--spec", spec_filepath]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') lv1_names = set([node.name for node in context.root.ls()]) lv2_names = set([node.name for node in context.root.ls('api')]) lv3_names = set([node.name for node in context.root.ls('api', 'v1')]) self.assertEqual(lv1_names, set(['api'])) self.assertEqual(lv2_names, set(['v1'])) self.assertEqual(lv3_names, set(['users', 'orgs'])) def test_spec_from_http(self): spec_url = 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json' result, context = run_and_exit(['https://api.github.com', '--spec', spec_url]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'https://api.github.com') top_level_paths = set([n.name for n in context.root.children]) self.assertIn('repos', top_level_paths) self.assertIn('users', top_level_paths) def test_spec_from_http_only(self): spec_url = ( 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.json') result, context = run_and_exit(['--spec', spec_url]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'https://api.medium.com/v1') lv1_names = set([node.name for node in context.root.ls()]) lv2_names = set([node.name for node in context.root.ls('v1')]) self.assertEqual(lv1_names, set(['v1'])) self.assertEqual(lv2_names, set(['me', 'publications', 'users'])) def test_spec_with_trailing_slash(self): spec_filepath = self.make_tempfile(json.dumps({ 'basePath': '/api', 'paths': { '/': {}, '/users/': {} } })) result, context = run_and_exit(['example.com', "--spec", spec_filepath]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') lv1_names = set([node.name for node in context.root.ls()]) lv2_names = set([node.name for node in context.root.ls('api')]) self.assertEqual(lv1_names, set(['api'])) self.assertEqual(lv2_names, set(['/', 'users/'])) def test_env_only(self): env_filepath = self.make_tempfile( "cd http://example.com\nname=bob\nid==10") result, context = run_and_exit(["--env", env_filepath]) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'bob'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) def test_env_with_url(self): env_filepath = self.make_tempfile( "cd http://example.com\nname=bob\nid==10") result, context = run_and_exit(["--env", env_filepath, 'other_example.com']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://other_example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'bob'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) def test_env_with_options(self): env_filepath = self.make_tempfile( "cd http://example.com\nname=bob\nid==10") result, context = run_and_exit(["--env", env_filepath, 'other_example.com', 'name=alice']) self.assertEqual(result.exit_code, 0) self.assertEqual(context.url, 'http://other_example.com') self.assertEqual(context.options, {}) self.assertEqual(context.body_params, {'name': 'alice'}) self.assertEqual(context.headers, {}) self.assertEqual(context.querystring_params, {'id': ['10']}) @patch('http_prompt.cli.prompt') @patch('http_prompt.cli.execute') def test_press_ctrl_d(self, execute_mock, prompt_mock): prompt_mock.side_effect = EOFError execute_mock.side_effect = execute result = CliRunner().invoke(cli, []) self.assertEqual(result.exit_code, 0) class TestExecutionListenerSetCookies(unittest.TestCase): def setUp(self): self.listener = ExecutionListener({}) self.response = Response() self.response.cookies.update({ 'username': 'john', 'sessionid': 'abcd' }) self.context = Context('http://localhost') self.context.headers['Cookie'] = 'name="John Doe"; sessionid=xyz' def test_auto(self): self.listener.cfg['set_cookies'] = 'auto' self.listener.response_returned(self.context, self.response) self.assertEqual(self.context.headers['Cookie'], 'name="John Doe"; sessionid=abcd; username=john') @patch('http_prompt.cli.click.confirm') def test_ask_and_yes(self, confirm_mock): confirm_mock.return_value = True self.listener.cfg['set_cookies'] = 'ask' self.listener.response_returned(self.context, self.response) self.assertEqual(self.context.headers['Cookie'], 'name="John Doe"; sessionid=abcd; username=john') @patch('http_prompt.cli.click.confirm') def test_ask_and_no(self, confirm_mock): confirm_mock.return_value = False self.listener.cfg['set_cookies'] = 'ask' self.listener.response_returned(self.context, self.response) self.assertEqual(self.context.headers['Cookie'], 'name="John Doe"; sessionid=xyz') def test_off(self): self.listener.cfg['set_cookies'] = 'off' self.listener.response_returned(self.context, self.response) self.assertEqual(self.context.headers['Cookie'], 'name="John Doe"; sessionid=xyz') ================================================ FILE: tests/test_completer.py ================================================ # -*- coding: utf-8 -*- import unittest from prompt_toolkit.document import Document from http_prompt.completer import HttpPromptCompleter from http_prompt.context import Context class TestCompleter(unittest.TestCase): def setUp(self): self.context = Context('http://localhost', spec={ 'paths': { '/users': {}, '/users/{username}': {}, '/users/{username}/events': {}, '/users/{username}/orgs': {}, '/orgs': {}, '/orgs/{org}': {}, '/orgs/{org}/events': {}, '/orgs/{org}/members': {} } }) self.completer = HttpPromptCompleter(self.context) self.completer_event = None def get_completions(self, command): if not isinstance(command, str): command = command.decode() position = len(command) completions = self.completer.get_completions( Document(text=command, cursor_position=position), self.completer_event) return [c.text for c in completions] def test_header_name(self): result = self.get_completions('ctype') self.assertEqual(result[0], 'Content-Type') def test_header_value(self): result = self.get_completions('Content-Type:json') self.assertEqual(result[0], 'application/json') def test_verify_option(self): result = self.get_completions('--vfy') self.assertEqual(result[0], '--verify') def test_preview_then_action(self): result = self.get_completions('httpie po') self.assertEqual(result[0], 'post') def test_rm_body_param(self): self.context.body_params['my_name'] = 'dont_care' result = self.get_completions('rm -b ') self.assertEqual(result[0], 'my_name') def test_rm_body_json_param(self): self.context.body_json_params['number'] = 2 result = self.get_completions('rm -b ') self.assertEqual(result[0], 'number') def test_rm_querystring_param(self): self.context.querystring_params['my_name'] = 'dont_care' result = self.get_completions('rm -q ') self.assertEqual(result[0], 'my_name') def test_rm_header(self): self.context.headers['Accept'] = 'dont_care' result = self.get_completions('rm -h ') self.assertEqual(result[0], 'Accept') def test_rm_option(self): self.context.options['--form'] = None result = self.get_completions('rm -o ') self.assertEqual(result[0], '--form') def test_querystring_with_chinese(self): result = self.get_completions('name==王') self.assertFalse(result) def test_header_with_spanish(self): result = self.get_completions('X-Custom-Header:Jesú') self.assertFalse(result) def test_options_method(self): result = self.get_completions('opt') self.assertEqual(result[0], 'options') def test_ls_no_path(self): result = self.get_completions('ls ') self.assertEqual(result, ['orgs', 'users']) def test_ls_no_path_substring(self): result = self.get_completions('ls o') self.assertEqual(result, ['orgs']) def test_ls_absolute_path(self): result = self.get_completions('ls /users/1/') self.assertEqual(result, ['events', 'orgs']) def test_ls_absolute_path_substring(self): result = self.get_completions('ls /users/1/e') self.assertEqual(result, ['events']) def test_ls_relative_path(self): self.context.url = 'http://localhost/orgs' result = self.get_completions('ls 1/') self.assertEqual(result, ['events', 'members']) def test_cd_no_path(self): result = self.get_completions('cd ') self.assertEqual(result, ['orgs', 'users']) def test_cd_no_path_substring(self): result = self.get_completions('cd o') self.assertEqual(result, ['orgs']) def test_cd_absolute_path(self): result = self.get_completions('cd /users/1/') self.assertEqual(result, ['events', 'orgs']) def test_cd_absolute_path_substring(self): result = self.get_completions('cd /users/1/e') self.assertEqual(result, ['events']) def test_cd_relative_path(self): self.context.url = 'http://localhost/orgs' result = self.get_completions('cd 1/') self.assertEqual(result, ['events', 'members']) ================================================ FILE: tests/test_config.py ================================================ import hashlib import os from .base import TempAppDirTestCase from http_prompt import config def _hash_file(path): with open(path, 'rb') as f: data = f.read() return hashlib.sha1(data).hexdigest() class TestConfig(TempAppDirTestCase): def test_initialize(self): # Config file doesn't exist at first expected_path = config.get_user_config_path() self.assertFalse(os.path.exists(expected_path)) # Config file should exist after initialization copied, actual_path = config.initialize() self.assertTrue(copied) self.assertEqual(actual_path, expected_path) self.assertTrue(os.path.exists(expected_path)) # Change config file and hash the content to see if it's changed with open(expected_path, 'a') as f: f.write('dont_care\n') orig_hash = _hash_file(expected_path) # Make sure it's fine to call config.initialize() twice copied, actual_path = config.initialize() self.assertFalse(copied) self.assertEqual(actual_path, expected_path) self.assertTrue(os.path.exists(expected_path)) # Make sure config file is unchanged new_hash = _hash_file(expected_path) self.assertEqual(new_hash, orig_hash) def test_load_default(self): cfg = config.load_default() self.assertEqual(cfg['command_style'], 'solarized') self.assertFalse(cfg['output_style']) self.assertEqual(cfg['pager'], 'less') def test_load_user(self): copied, path = config.initialize() self.assertTrue(copied) with open(path, 'w') as f: f.write("\ngreeting = 'hello!'\n") cfg = config.load_user() self.assertEqual(cfg, {'greeting': 'hello!'}) def test_load(self): copied, path = config.initialize() self.assertTrue(copied) with open(path, 'w') as f: f.write("pager = 'more'\n" "greeting = 'hello!'\n") cfg = config.load() self.assertEqual(cfg['command_style'], 'solarized') self.assertFalse(cfg['output_style']) self.assertEqual(cfg['pager'], 'more') self.assertEqual(cfg['greeting'], 'hello!') ================================================ FILE: tests/test_contextio.py ================================================ # -*- coding: utf-8 -*- from .base import TempAppDirTestCase from http_prompt.context import Context from http_prompt.contextio import save_context, load_context class TestContextIO(TempAppDirTestCase): def test_save_and_load_context_non_ascii(self): c = Context('http://localhost') c.headers.update({ 'User-Agent': 'Ö', 'Authorization': '中文' }) save_context(c) c = Context('http://0.0.0.0') load_context(c) self.assertEqual(c.url, 'http://localhost') self.assertEqual(c.headers, { 'User-Agent': 'Ö', 'Authorization': '中文' }) ================================================ FILE: tests/test_execution.py ================================================ # -*- coding: utf-8 -*- import hashlib import io import json import shutil import os import sys import pytest from collections import namedtuple from unittest.mock import patch from http_prompt.context import Context from http_prompt.execution import execute, HTTPIE_PROGRAM_NAME from .base import TempAppDirTestCase class ExecutionTestCase(TempAppDirTestCase): def setUp(self): super(ExecutionTestCase, self).setUp() self.patchers = [ ('httpie_main', patch('http_prompt.execution.httpie_main')), ('echo_via_pager', patch('http_prompt.output.click.echo_via_pager')), ('secho', patch('http_prompt.execution.click.secho')), ('get_terminal_size', patch('http_prompt.utils.get_terminal_size')) ] for attr_name, patcher in self.patchers: setattr(self, attr_name, patcher.start()) self.context = Context('http://localhost', spec={ 'paths': { '/users': {}, '/users/{username}': {}, '/users/{username}/events': {}, '/users/{username}/orgs': {}, '/orgs': {}, '/orgs/{org}': {}, '/orgs/{org}/events': {}, '/orgs/{org}/members': {} } }) # pytest mocks to capture stdout so we can't really get_terminal_size() Size = namedtuple('Size', ['columns', 'rows']) self.get_terminal_size.return_value = Size(80, 30) def tearDown(self): super(ExecutionTestCase, self).tearDown() for _, patcher in self.patchers: patcher.stop() def assert_httpie_main_called_with(self, args): self.assertEqual(self.httpie_main.call_args[0][0], [ HTTPIE_PROGRAM_NAME, *args]) def assert_stdout(self, expected_msg): # Append '\n' to simulate behavior of click.echo_via_pager(), # which we use whenever we want to output anything to stdout printed_msg = self.echo_via_pager.call_args[0][0] + '\n' self.assertEqual(printed_msg, expected_msg) def assert_stdout_startswith(self, expected_prefix): printed_msg = self.echo_via_pager.call_args[0][0] self.assertTrue(printed_msg.startswith(expected_prefix)) def get_stdout(self): return self.echo_via_pager.call_args[0][0] def assert_stderr(self, expected_msg): printed_msg = self.secho.call_args[0][0] print_options = self.secho.call_args[1] self.assertEqual(printed_msg, expected_msg) self.assertEqual(print_options, {'err': True, 'fg': 'red'}) class TestExecution_noop(ExecutionTestCase): def test_empty_string(self): execute('', self.context) self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.options) self.assertFalse(self.context.headers) self.assertFalse(self.context.querystring_params) self.assertFalse(self.context.body_params) self.assertFalse(self.context.should_exit) def test_spaces(self): execute(' \t \t ', self.context) self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.options) self.assertFalse(self.context.headers) self.assertFalse(self.context.querystring_params) self.assertFalse(self.context.body_params) self.assertFalse(self.context.should_exit) class TestExecution_env(ExecutionTestCase): def setUp(self): super(TestExecution_env, self).setUp() self.context.url = 'http://localhost:8000/api' self.context.headers.update({ 'Accept': 'text/csv', 'Authorization': 'ApiKey 1234' }) self.context.querystring_params.update({ 'page': ['1'], 'limit': ['50'] }) self.context.body_params.update({ 'name': 'John Doe' }) self.context.options.update({ '--verify': 'no', '--form': None }) def test_env(self): execute('env', self.context) self.assert_stdout("--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_with_spaces(self): execute(' env ', self.context) self.assert_stdout("--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_non_ascii(self): self.context.body_params['name'] = '許 功蓋' execute('env', self.context) self.assert_stdout("--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=許 功蓋'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_write_to_file(self): filename = self.make_tempfile() # write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('env > %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, "--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_write_to_file_with_env_vars(self): filename = self.make_tempfile('hello world\n', 'testenvvar') filename_with_var = filename.replace("testenvvar", "${MYPRIVATEVAR}") os.environ['MYPRIVATEVAR'] = 'testenvvar' execute('env > %s' % filename_with_var, self.context) os.environ['MYPRIVATEVAR'] = '' with open(filename) as f: content = f.read() self.assertEqual(content, "--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_non_ascii_and_write_to_file(self): filename = self.make_tempfile() # write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') self.context.body_params['name'] = '許 功蓋' execute('env > %s' % filename, self.context) with open(filename, encoding='utf-8') as f: content = f.read() self.assertEqual(content, "--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=許 功蓋'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_write_to_quoted_filename(self): filename = self.make_tempfile() # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute("env > '%s'" % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, "--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") def test_env_append_to_file(self): filename = self.make_tempfile() # Write something first to make sure it's an append with open(filename, 'w') as f: f.write('hello world\n') execute('env >> %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, "hello world\n" "--form\n--verify=no\n" "cd http://localhost:8000/api\n" "limit==50\npage==1\n" "'name=John Doe'\n" "Accept:text/csv\n" "'Authorization:ApiKey 1234'\n") class TestExecution_source_and_exec(ExecutionTestCase): def setUp(self): super(TestExecution_source_and_exec, self).setUp() self.context.url = 'http://localhost:8000/api' self.context.headers.update({ 'Accept': 'text/csv', 'Authorization': 'ApiKey 1234' }) self.context.querystring_params.update({ 'page': ['1'], 'limit': ['50'] }) self.context.body_params.update({ 'name': 'John Doe' }) self.context.options.update({ '--verify': 'no', '--form': None }) # The file that is about to be sourced/exec'd self.filename = self.make_tempfile( "Language:en Authorization:'ApiKey 5678'\n" "name='Jane Doe' username=jane limit==25\n" "rm -o --form\n" "cd v2/user\n") def test_source(self): execute('source %s' % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Accept': 'text/csv', 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) self.assertEqual(self.context.options, { '--verify': 'no' }) def test_source_with_spaces(self): execute(' source %s ' % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Accept': 'text/csv', 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) self.assertEqual(self.context.options, { '--verify': 'no' }) def test_source_non_existing_file(self): c = self.context.copy() execute('source no_such_file.txt', self.context) self.assertEqual(self.context, c) # Expect the error message would be the same as when we open the # non-existing file try: with open('no_such_file.txt'): pass except OSError as err: err_msg = str(err) else: assert False, 'what?! no_such_file.txt exists!' self.assert_stderr(err_msg) def test_source_quoted_filename(self): execute('source "%s"' % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Accept': 'text/csv', 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) self.assertEqual(self.context.options, { '--verify': 'no' }) @pytest.mark.skipif(sys.platform == 'win32', reason="Windows doesn't use backslashes to escape") def test_source_escaped_filename(self): new_filename = self.filename + r' copy' shutil.copyfile(self.filename, new_filename) new_filename = new_filename.replace(' ', r'\ ') execute('source %s' % new_filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Accept': 'text/csv', 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) self.assertEqual(self.context.options, { '--verify': 'no' }) def test_exec(self): execute('exec %s' % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) def test_exec_with_spaces(self): execute(' exec %s ' % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) def test_exec_non_existing_file(self): c = self.context.copy() execute('exec no_such_file.txt', self.context) self.assertEqual(self.context, c) # Try to get the error message when opening a non-existing file try: with open('no_such_file.txt'): pass except OSError as err: err_msg = str(err) else: assert False, 'what?! no_such_file.txt exists!' self.assert_stderr(err_msg) def test_exec_quoted_filename(self): execute("exec '%s'" % self.filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) @pytest.mark.skipif(sys.platform == 'win32', reason="Windows doesn't use backslashes to escape") def test_exec_escaped_filename(self): new_filename = self.filename + r' copy' shutil.copyfile(self.filename, new_filename) new_filename = new_filename.replace(' ', r'\ ') execute('exec %s' % new_filename, self.context) self.assertEqual(self.context.url, 'http://localhost:8000/api/v2/user') self.assertEqual(self.context.headers, { 'Authorization': 'ApiKey 5678', 'Language': 'en' }) self.assertEqual(self.context.querystring_params, { 'limit': ['25'] }) self.assertEqual(self.context.body_params, { 'name': 'Jane Doe', 'username': 'jane' }) class TestExecution_env_and_source(ExecutionTestCase): def test_env_and_source(self): c = Context() c.url = 'http://localhost:8000/api' c.headers.update({ 'Accept': 'text/csv', 'Authorization': 'ApiKey 1234' }) c.querystring_params.update({ 'page': ['1'], 'limit': ['50'] }) c.body_params.update({ 'name': 'John Doe' }) c.options.update({ '--verify': 'no', '--form': None }) c2 = c.copy() filename = self.make_tempfile() execute('env > %s' % filename, c) execute('rm *', c) self.assertFalse(c.headers) self.assertFalse(c.querystring_params) self.assertFalse(c.body_params) self.assertFalse(c.options) execute('source %s' % filename, c) self.assertEqual(c, c2) def test_env_and_source_non_ascii(self): c = Context() c.url = 'http://localhost:8000/api' c.headers.update({ 'Accept': 'text/csv', 'Authorization': 'ApiKey 1234' }) c.querystring_params.update({ 'page': ['1'], 'limit': ['50'] }) c.body_params.update({ 'name': '許 功蓋' }) c.options.update({ '--verify': 'no', '--form': None }) c2 = c.copy() filename = self.make_tempfile() execute('env > %s' % filename, c) execute('rm *', c) self.assertFalse(c.headers) self.assertFalse(c.querystring_params) self.assertFalse(c.body_params) self.assertFalse(c.options) execute('source %s' % filename, c) self.assertEqual(c, c2) class TestExecution_help(ExecutionTestCase): def test_help(self): execute('help', self.context) self.assert_stdout_startswith('Commands:\n\tcd') def test_help_with_spaces(self): execute(' help ', self.context) self.assert_stdout_startswith('Commands:\n\tcd') class TestExecution_exit(ExecutionTestCase): def test_exit(self): execute('exit', self.context) self.assertTrue(self.context.should_exit) def test_exit_with_spaces(self): execute(' exit ', self.context) self.assertTrue(self.context.should_exit) class TestExecution_cd(ExecutionTestCase): def test_single_level(self): execute('cd api', self.context) self.assertEqual(self.context.url, 'http://localhost/api') def test_many_levels(self): execute('cd api/v2/movie/50', self.context) self.assertEqual(self.context.url, 'http://localhost/api/v2/movie/50') def test_change_base(self): execute('cd //example.com/api', self.context) self.assertEqual(self.context.url, 'http://example.com/api') def test_root(self): execute('cd /api/v2', self.context) self.assertEqual(self.context.url, 'http://localhost/api/v2') execute('cd /index.html', self.context) self.assertEqual(self.context.url, 'http://localhost/index.html') def test_dot_dot(self): execute('cd api/v1', self.context) self.assertEqual(self.context.url, 'http://localhost/api/v1') execute('cd ..', self.context) self.assertEqual(self.context.url, 'http://localhost/api') # If dot-dot has a trailing slash, the resulting URL should have a # trailing slash execute('cd ../rest/api/', self.context) self.assertEqual(self.context.url, 'http://localhost/rest/api/') def test_url_with_trailing_slash(self): self.context.url = 'http://localhost/' execute('cd api', self.context) self.assertEqual(self.context.url, 'http://localhost/api') execute('cd v2/', self.context) self.assertEqual(self.context.url, 'http://localhost/api/v2/') execute('cd /objects/', self.context) self.assertEqual(self.context.url, 'http://localhost/objects/') def test_path_with_trailing_slash(self): execute('cd api/', self.context) self.assertEqual(self.context.url, 'http://localhost/api/') execute('cd movie/1/', self.context) self.assertEqual(self.context.url, 'http://localhost/api/movie/1/') def test_without_url(self): execute('cd api/', self.context) self.assertEqual(self.context.url, 'http://localhost/api/') execute('cd', self.context) self.assertEqual(self.context.url, 'http://localhost') class TestExecution_rm(ExecutionTestCase): def test_header(self): self.context.headers['Content-Type'] = 'text/html' execute('rm -h Content-Type', self.context) self.assertFalse(self.context.headers) def test_option(self): self.context.options['--form'] = None execute('rm -o --form', self.context) self.assertFalse(self.context.options) def test_querystring(self): self.context.querystring_params['page'] = '1' execute('rm -q page', self.context) self.assertFalse(self.context.querystring_params) def test_body_param(self): self.context.body_params['name'] = 'alice' execute('rm -b name', self.context) self.assertFalse(self.context.body_params) def test_body_json_param(self): self.context.body_json_params['name'] = 'bob' execute('rm -b name', self.context) self.assertFalse(self.context.body_json_params) def test_header_single_quoted(self): self.context.headers['Content-Type'] = 'text/html' execute("rm -h 'Content-Type'", self.context) self.assertFalse(self.context.headers) def test_option_double_quoted(self): self.context.options['--form'] = None execute('rm -o "--form"', self.context) self.assertFalse(self.context.options) def test_querystring_double_quoted(self): self.context.querystring_params['page size'] = '10' execute('rm -q "page size"', self.context) self.assertFalse(self.context.querystring_params) def test_body_param_double_quoted(self): self.context.body_params['family name'] = 'Doe Doe' execute('rm -b "family name"', self.context) self.assertFalse(self.context.body_params) def test_body_param_escaped(self): self.context.body_params['family name'] = 'Doe Doe' execute(r'rm -b family\ name', self.context) self.assertFalse(self.context.body_params) def test_body_json_param_escaped_colon(self): self.context.body_json_params[r'where[id\:gt]'] = 2 execute(r'rm -b where[id\:gt]', self.context) self.assertFalse(self.context.body_json_params) def test_body_param_escaped_equal(self): self.context.body_params[r'foo\=bar'] = 'hello' execute(r'rm -b foo\=bar', self.context) self.assertFalse(self.context.body_params) def test_non_existing_key(self): execute('rm -q abcd', self.context) self.assert_stderr("Key 'abcd' not found") def test_non_existing_key_unicode(self): # See #25 execute(u'rm -q abcd', self.context) self.assert_stderr("Key 'abcd' not found") def test_body_reset(self): self.context.body_params.update({ 'first_name': 'alice', 'last_name': 'bryne' }) execute('rm -b *', self.context) self.assertFalse(self.context.body_params) def test_querystring_reset(self): self.context.querystring_params.update({ 'first_name': 'alice', 'last_name': 'bryne' }) execute('rm -q *', self.context) self.assertFalse(self.context.querystring_params) def test_headers_reset(self): self.context.headers.update({ 'Content-Type': 'text/html', 'Accept': 'application/json' }) execute('rm -h *', self.context) self.assertFalse(self.context.headers) def test_options_reset(self): self.context.options.update({ '--form': None, '--body': None }) execute('rm -o *', self.context) self.assertFalse(self.context.options) def test_reset(self): self.context.options.update({ '--form': None, '--verify': 'no' }) self.context.headers.update({ 'Accept': 'dontcare', 'Content-Type': 'dontcare' }) self.context.querystring_params.update({ 'name': 'dontcare', 'email': 'dontcare' }) self.context.body_params.update({ 'name': 'dontcare', 'email': 'dontcare' }) self.context.body_json_params.update({ 'name': 'dontcare' }) execute('rm *', self.context) self.assertFalse(self.context.options) self.assertFalse(self.context.headers) self.assertFalse(self.context.querystring_params) self.assertFalse(self.context.body_params) self.assertFalse(self.context.body_json_params) class TestExecution_ls(ExecutionTestCase): def test_root(self): execute('ls', self.context) self.assert_stdout('orgs users\n') def test_relative_path(self): self.context.url = 'http://localhost/users' execute('ls 101', self.context) self.assert_stdout('events orgs\n') def test_absolute_path(self): self.context.url = 'http://localhost/users' execute('ls /orgs/1', self.context) self.assert_stdout('events members\n') def test_redirect_write(self): filename = self.make_tempfile() # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('ls > %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'orgs\nusers') def test_redirect_append(self): filename = self.make_tempfile() # Write something first to make sure it's an append with open(filename, 'w') as f: f.write('hello world\n') execute('ls >> %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'hello world\norgs\nusers') def test_grep(self): execute('ls | grep users', self.context) self.assert_stdout('users\n') class TestMutation(ExecutionTestCase): def test_simple_headers(self): execute('Accept:text/html User-Agent:HttpPrompt', self.context) self.assertEqual(self.context.headers, { 'Accept': 'text/html', 'User-Agent': 'HttpPrompt' }) def test_header_value_with_double_quotes(self): execute('Accept:text/html User-Agent:"HTTP Prompt"', self.context) self.assertEqual(self.context.headers, { 'Accept': 'text/html', 'User-Agent': 'HTTP Prompt' }) def test_header_value_with_single_quotes(self): execute("Accept:text/html User-Agent:'HTTP Prompt'", self.context) self.assertEqual(self.context.headers, { 'Accept': 'text/html', 'User-Agent': 'HTTP Prompt' }) def test_header_with_double_quotes(self): execute('Accept:text/html "User-Agent:HTTP Prompt"', self.context) self.assertEqual(self.context.headers, { 'Accept': 'text/html', 'User-Agent': 'HTTP Prompt' }) def test_header_with_single_quotes(self): execute("Accept:text/html 'User-Agent:HTTP Prompt'", self.context) self.assertEqual(self.context.headers, { 'Accept': 'text/html', 'User-Agent': 'HTTP Prompt' }) def test_header_escaped_chars(self): execute(r'X-Name:John\'s\ Doe', self.context) self.assertEqual(self.context.headers, { 'X-Name': "John's Doe" }) def test_header_value_escaped_quote(self): execute(r"'X-Name:John\'s Doe'", self.context) self.assertEqual(self.context.headers, { 'X-Name': "John's Doe" }) def test_simple_querystring(self): execute('page==1 limit==20', self.context) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'limit': ['20'] }) def test_querystring_with_double_quotes(self): execute('page==1 name=="John Doe"', self.context) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'name': ['John Doe'] }) def test_querystring_with_single_quotes(self): execute("page==1 name=='John Doe'", self.context) self.assertEqual(self.context.querystring_params, { 'page': ['1'], 'name': ['John Doe'] }) def test_querystring_with_chinese(self): execute("name==王小明", self.context) self.assertEqual(self.context.querystring_params, { 'name': ['王小明'] }) def test_querystring_escaped_chars(self): execute(r'name==John\'s\ Doe', self.context) self.assertEqual(self.context.querystring_params, { 'name': ["John's Doe"] }) def test_querytstring_value_escaped_quote(self): execute(r"'name==John\'s Doe'", self.context) self.assertEqual(self.context.querystring_params, { 'name': ["John's Doe"] }) def test_querystring_key_escaped_quote(self): execute(r"'john\'s last name==Doe'", self.context) self.assertEqual(self.context.querystring_params, { "john's last name": ['Doe'] }) def test_simple_body_params(self): execute('username=john password=123', self.context) self.assertEqual(self.context.body_params, { 'username': 'john', 'password': '123' }) def test_body_param_value_with_double_quotes(self): execute('name="John Doe" password=123', self.context) self.assertEqual(self.context.body_params, { 'name': 'John Doe', 'password': '123' }) def test_body_param_value_with_single_quotes(self): execute("name='John Doe' password=123", self.context) self.assertEqual(self.context.body_params, { 'name': 'John Doe', 'password': '123' }) def test_body_param_with_double_quotes(self): execute('"name=John Doe" password=123', self.context) self.assertEqual(self.context.body_params, { 'name': 'John Doe', 'password': '123' }) def test_body_param_with_spanish(self): execute('name=Jesús', self.context) self.assertEqual(self.context.body_params, { 'name': 'Jesús' }) def test_body_param_escaped_chars(self): execute(r'name=John\'s\ Doe', self.context) self.assertEqual(self.context.body_params, { 'name': "John's Doe" }) def test_body_param_value_escaped_quote(self): execute(r"'name=John\'s Doe'", self.context) self.assertEqual(self.context.body_params, { 'name': "John's Doe" }) def test_body_param_key_escaped_quote(self): execute(r"'john\'s last name=Doe'", self.context) self.assertEqual(self.context.body_params, { "john's last name": 'Doe' }) def test_long_option_names(self): execute('--auth user:pass --form', self.context) self.assertEqual(self.context.options, { '--form': None, '--auth': 'user:pass' }) def test_long_option_names_with_its_prefix(self): execute('--auth-type basic --auth user:pass --session user ' '--session-read-only user', self.context) self.assertEqual(self.context.options, { '--auth-type': 'basic', '--auth': 'user:pass', '--session-read-only': 'user', '--session': 'user' }) def test_long_short_option_names_mixed(self): execute('--style=default -j --stream', self.context) self.assertEqual(self.context.options, { '-j': None, '--stream': None, '--style': 'default' }) def test_option_and_body_param(self): execute('--form name="John Doe"', self.context) self.assertEqual(self.context.options, { '--form': None }) self.assertEqual(self.context.body_params, { 'name': 'John Doe' }) def test_mixed(self): execute(' --form name="John Doe" password=1234\\ 5678 ' 'User-Agent:HTTP\\ Prompt -a \'john:1234 5678\' ' '"Accept:text/html" ', self.context) self.assertEqual(self.context.options, { '--form': None, '-a': 'john:1234 5678' }) self.assertEqual(self.context.headers, { 'User-Agent': 'HTTP Prompt', 'Accept': 'text/html' }) self.assertEqual(self.context.options, { '--form': None, '-a': 'john:1234 5678' }) self.assertEqual(self.context.body_params, { 'name': 'John Doe', 'password': '1234 5678' }) def test_multi_querystring(self): execute('name==john name==doe', self.context) self.assertEqual(self.context.querystring_params, { 'name': ['john', 'doe'] }) execute('name==jane', self.context) self.assertEqual(self.context.querystring_params, { 'name': ['jane'] }) def test_raw_json_object(self): execute("""definition:={"id":819,"name":"ML"}""", self.context) self.assertEqual(self.context.body_json_params, { 'definition': { 'id': 819, 'name': 'ML' } }) def test_raw_json_object_quoted(self): execute("""definition:='{"id": 819, "name": "ML"}'""", self.context) self.assertEqual(self.context.body_json_params, { 'definition': { 'id': 819, 'name': 'ML' } }) def test_raw_json_array(self): execute("""names:=["foo","bar"]""", self.context) self.assertEqual(self.context.body_json_params, { 'names': ["foo", "bar"] }) def test_raw_json_array_quoted(self): execute("""names:='["foo", "bar"]'""", self.context) self.assertEqual(self.context.body_json_params, { 'names': ["foo", "bar"] }) def test_raw_json_integer(self): execute('number:=999', self.context) self.assertEqual(self.context.body_json_params, {'number': 999}) def test_raw_json_string(self): execute("""name:='"john doe"'""", self.context) self.assertEqual(self.context.body_json_params, {'name': 'john doe'}) def test_escape_colon(self): execute(r'where[id\:gt]:=2', self.context) self.assertEqual(self.context.body_json_params, { r'where[id\:gt]': 2 }) def test_escape_equal(self): execute(r'foo\=bar=hello', self.context) self.assertEqual(self.context.body_params, { r'foo\=bar': 'hello' }) class TestHttpAction(ExecutionTestCase): def test_get(self): execute('get', self.context) self.assert_httpie_main_called_with(['GET', 'http://localhost']) def test_get_uppercase(self): execute('GET', self.context) self.assert_httpie_main_called_with(['GET', 'http://localhost']) def test_get_multi_querystring(self): execute('get foo==1 foo==2 foo==3', self.context) self.assert_httpie_main_called_with([ 'GET', 'http://localhost', 'foo==1', 'foo==2', 'foo==3']) def test_post(self): execute('post page==1', self.context) self.assert_httpie_main_called_with(['POST', 'http://localhost', 'page==1']) self.assertFalse(self.context.querystring_params) def test_post_with_absolute_path(self): execute('post /api/v3 name=bob', self.context) self.assert_httpie_main_called_with(['POST', 'http://localhost/api/v3', 'name=bob']) self.assertFalse(self.context.body_params) self.assertEqual(self.context.url, 'http://localhost') def test_post_with_relative_path(self): self.context.url = 'http://localhost/api/v3' execute('post ../v2/movie id=8', self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost/api/v2/movie', 'id=8']) self.assertFalse(self.context.body_params) self.assertEqual(self.context.url, 'http://localhost/api/v3') def test_post_with_full_url(self): execute('post http://httpbin.org/post id=9', self.context) self.assert_httpie_main_called_with([ 'POST', 'http://httpbin.org/post', 'id=9']) self.assertFalse(self.context.body_params) self.assertEqual(self.context.url, 'http://localhost') def test_post_with_full_https_url(self): execute('post https://httpbin.org/post id=9', self.context) self.assert_httpie_main_called_with([ 'POST', 'https://httpbin.org/post', 'id=9']) self.assertFalse(self.context.body_params) self.assertEqual(self.context.url, 'http://localhost') def test_post_uppercase(self): execute('POST content=text', self.context) self.assert_httpie_main_called_with(['POST', 'http://localhost', 'content=text']) self.assertFalse(self.context.body_params) def test_post_raw_json_object(self): execute("""post definition:={"id":819,"name":"ML"}""", self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', """definition:={"id": 819, "name": "ML"}"""]) self.assertFalse(self.context.body_json_params) def test_post_raw_json_object_quoted(self): execute("""post definition:='{"id": 819, "name": "ML"}'""", self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', 'definition:={"id": 819, "name": "ML"}']) self.assertFalse(self.context.body_json_params) def test_post_raw_json_array(self): execute("""post hobbies:=["foo","bar"]""", self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', 'hobbies:=["foo", "bar"]']) self.assertFalse(self.context.body_json_params) def test_post_raw_json_array_quoted(self): execute("""post hobbies:='["foo", "bar"]'""", self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', 'hobbies:=["foo", "bar"]']) self.assertFalse(self.context.body_json_params) def test_post_raw_json_integer(self): execute('post number:=123', self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', 'number:=123']) self.assertFalse(self.context.body_json_params) def test_post_raw_json_boolean(self): execute('post foo:=true', self.context) self.assert_httpie_main_called_with([ 'POST', 'http://localhost', 'foo:=true']) self.assertFalse(self.context.body_json_params) def test_delete(self): execute('delete', self.context) self.assert_httpie_main_called_with(['DELETE', 'http://localhost']) def test_delete_uppercase(self): execute('DELETE', self.context) self.assert_httpie_main_called_with(['DELETE', 'http://localhost']) def test_patch(self): execute('patch', self.context) self.assert_httpie_main_called_with(['PATCH', 'http://localhost']) def test_patch_uppercase(self): execute('PATCH', self.context) self.assert_httpie_main_called_with(['PATCH', 'http://localhost']) def test_head(self): execute('head', self.context) self.assert_httpie_main_called_with(['HEAD', 'http://localhost']) def test_head_uppercase(self): execute('HEAD', self.context) self.assert_httpie_main_called_with(['HEAD', 'http://localhost']) def test_options(self): execute('options', self.context) self.assert_httpie_main_called_with(['OPTIONS', 'http://localhost']) class TestHttpActionRedirection(ExecutionTestCase): def test_get(self): execute('get > data.json', self.context) self.assert_httpie_main_called_with(['GET', 'http://localhost']) env = self.httpie_main.call_args[1]['env'] self.assertFalse(env.stdout_isatty) self.assertEqual(env.stdout.fp.name, 'data.json') @pytest.mark.slow class TestHttpBin(TempAppDirTestCase): """Send real requests to http://httpbin.org, save the responses to files, and asserts on the file content. """ def setUp(self): super(TestHttpBin, self).setUp() # XXX: pytest doesn't allow HTTPie to read stdin while it's capturing # stdout, so we replace stdin with a file temporarily during the test. class MockStdin(object): def __init__(self, fp): self.fp = fp def isatty(self): return True def __getattr__(self, name): if name == 'isatty': return self.isatty return getattr(self.fp, name) self.orig_stdin = sys.stdin filename = self.make_tempfile() sys.stdin = MockStdin(open(filename, 'rb')) sys.stdin.isatty = lambda: True # Mock echo_via_pager() so that we can catch data fed to stdout self.patcher = patch('http_prompt.output.click.echo_via_pager') self.echo_via_pager = self.patcher.start() def tearDown(self): self.patcher.stop() sys.stdin.close() sys.stdin = self.orig_stdin super(TestHttpBin, self).tearDown() def get_stdout(self): return self.echo_via_pager.call_args[0][0] def execute_redirection(self, command): context = Context('http://httpbin.org') filename = self.make_tempfile() execute('%s > %s' % (command, filename), context) with open(filename, 'rb') as f: return f.read() def execute_pipe(self, command): context = Context('http://httpbin.org') execute(command, context) def test_get_image(self): data = self.execute_redirection('get /image/png') self.assertTrue(data) self.assertEqual(hashlib.sha1(data).hexdigest(), '379f5137831350c900e757b39e525b9db1426d53') def test_get_querystring(self): data = self.execute_redirection( 'get /get id==1234 X-Custom-Header:5678') data = json.loads(data.decode()) self.assertEqual(data['args'], { 'id': '1234' }) self.assertEqual(data['headers']['X-Custom-Header'], '5678') def test_post_json(self): data = self.execute_redirection( 'post /post id=1234 X-Custom-Header:5678') data = json.loads(data.decode()) self.assertEqual(data['json'], { 'id': '1234' }) self.assertEqual(data['headers']['X-Custom-Header'], '5678') def test_post_form(self): data = self.execute_redirection( 'post /post --form id=1234 X-Custom-Header:5678') data = json.loads(data.decode()) self.assertEqual(data['form'], { 'id': '1234' }) self.assertEqual(data['headers']['X-Custom-Header'], '5678') @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") def test_get_and_tee(self): filename = self.make_tempfile() self.execute_pipe('get /get hello==world | tee %s' % filename) with open(filename) as f: data = json.load(f) self.assertEqual(data['args'], {'hello': 'world'}) printed_msg = self.get_stdout() data = json.loads(printed_msg) self.assertEqual(data['args'], {'hello': 'world'}) class TestCommandPreview(ExecutionTestCase): def test_httpie_without_args(self): execute('httpie', self.context) self.assert_stdout('http http://localhost\n') def test_httpie_with_post(self): execute('httpie post name=alice', self.context) self.assert_stdout('http POST http://localhost name=alice\n') self.assertFalse(self.context.body_params) def test_httpie_with_absolute_path(self): execute('httpie post /api name=alice', self.context) self.assert_stdout('http POST http://localhost/api name=alice\n') self.assertFalse(self.context.body_params) def test_httpie_with_full_url(self): execute('httpie POST http://httpbin.org/post name=alice', self.context) self.assert_stdout('http POST http://httpbin.org/post name=alice\n') self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.body_params) def test_httpie_with_full_https_url(self): execute('httpie post https://httpbin.org/post name=alice', self.context) self.assert_stdout('http POST https://httpbin.org/post name=alice\n') self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.body_params) def test_httpie_with_quotes(self): execute(r'httpie post http://httpbin.org/post name="john doe" ' r"apikey==abc\ 123 'Authorization:ApiKey 1234'", self.context) self.assert_stdout( "http POST http://httpbin.org/post 'apikey==abc 123' " "'name=john doe' 'Authorization:ApiKey 1234'\n") self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.body_params) self.assertFalse(self.context.querystring_params) self.assertFalse(self.context.headers) def test_httpie_with_multi_querystring(self): execute('httpie get foo==1 foo==2 foo==3', self.context) self.assert_stdout('http GET http://localhost foo==1 foo==2 foo==3\n') self.assertEqual(self.context.url, 'http://localhost') self.assertFalse(self.context.querystring_params) class TestPipe(ExecutionTestCase): @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") def test_httpie_sed(self): execute("httpie get some==data | sed 's/data$/input/'", self.context) self.assert_stdout('http GET http://localhost some==input\n') @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") def test_httpie_sed_with_echo(self): execute("httpie post | `echo \"sed 's/localhost$/127.0.0.1/'\"`", self.context) self.assert_stdout("http POST http://127.0.0.1\n") @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") def test_env_grep(self): self.context.body_params = { 'username': 'jane', 'name': 'Jane', 'password': '1234' } execute('env | grep name', self.context) self.assert_stdout('name=Jane\nusername=jane\n') class TestShellSubstitution(ExecutionTestCase): def test_unquoted_option(self): execute("--auth `echo user:pass`", self.context) self.assertEqual(self.context.options, { '--auth': 'user:pass' }) def test_partial_unquoted_option(self): execute("--auth user:`echo pass`", self.context) self.assertEqual(self.context.options, { '--auth': 'user:pass' }) def test_partial_squoted_option(self): execute("--auth='user:`echo pass`'", self.context) self.assertEqual(self.context.options, { '--auth': 'user:pass' }) def test_partial_dquoted_option(self): execute('--auth="user:`echo pass`"', self.context) self.assertEqual(self.context.options, { '--auth': 'user:pass' }) def test_unquoted_header(self): execute("`echo 'X-Greeting'`:`echo 'hello world'`", self.context) if sys.platform == 'win32': expected_key = "'X-Greeting'" expected_value = "'hello world'" else: expected_key = 'X-Greeting' expected_value = 'hello world' self.assertEqual(self.context.headers, { expected_key: expected_value }) def test_full_squoted_header(self): execute("'`echo X-Greeting`:`echo hello`'", self.context) self.assertEqual(self.context.headers, { 'X-Greeting': 'hello' }) def test_full_dquoted_header(self): execute('"`echo X-Greeting`:`echo hello`"', self.context) self.assertEqual(self.context.headers, { 'X-Greeting': 'hello' }) def test_value_squoted_header(self): execute("`echo X-Greeting`:'`echo hello`'", self.context) self.assertEqual(self.context.headers, { 'X-Greeting': 'hello' }) def test_value_dquoted_header(self): execute('`echo X-Greeting`:"`echo hello`"', self.context) self.assertEqual(self.context.headers, { 'X-Greeting': 'hello' }) def test_partial_value_dquoted_header(self): execute('Authorization:"Bearer `echo OAUTH TOKEN`"', self.context) self.assertEqual(self.context.headers, { 'Authorization': 'Bearer OAUTH TOKEN' }) def test_partial_full_dquoted_header(self): execute('"Authorization:Bearer `echo OAUTH TOKEN`"', self.context) self.assertEqual(self.context.headers, { 'Authorization': 'Bearer OAUTH TOKEN' }) def test_unquoted_querystring(self): execute("`echo greeting`==`echo 'hello world'`", self.context) expected = ("'hello world'" if sys.platform == 'win32' else 'hello world') self.assertEqual(self.context.querystring_params, { 'greeting': [expected] }) def test_full_squoted_querystring(self): execute("'`echo greeting`==`echo hello`'", self.context) self.assertEqual(self.context.querystring_params, { 'greeting': ['hello'] }) def test_value_squoted_querystring(self): execute("`echo greeting`=='`echo hello`'", self.context) self.assertEqual(self.context.querystring_params, { 'greeting': ['hello'] }) def test_value_dquoted_querystring(self): execute('`echo greeting`=="`echo hello`"', self.context) self.assertEqual(self.context.querystring_params, { 'greeting': ['hello'] }) def test_unquoted_body_param(self): execute("`echo greeting`=`echo 'hello world'`", self.context) expected = ("'hello world'" if sys.platform == 'win32' else 'hello world') self.assertEqual(self.context.body_params, { 'greeting': expected }) def test_full_squoted_body_param(self): execute("'`echo greeting`=`echo hello`'", self.context) self.assertEqual(self.context.body_params, { 'greeting': 'hello' }) def test_value_squoted_body_param(self): execute("`echo greeting`='`echo hello`'", self.context) self.assertEqual(self.context.body_params, { 'greeting': 'hello' }) def test_full_dquoted_body_param(self): execute('"`echo greeting`=`echo hello`"', self.context) self.assertEqual(self.context.body_params, { 'greeting': 'hello' }) def test_bad_command(self): execute("name=`bad command test`", self.context) self.assertEqual(self.context.body_params, {'name': ''}) @pytest.mark.skipif(sys.platform == 'win32', reason="Unix only") def test_pipe_and_grep(self): execute("greeting=`echo 'hello world\nhihi\n' | grep hello`", self.context) self.assertEqual(self.context.body_params, { 'greeting': 'hello world' }) class TestCommandPreviewRedirection(ExecutionTestCase): def test_httpie_redirect_write(self): filename = self.make_tempfile() # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('httpie > %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'http http://localhost\n') def test_httpie_redirect_write_quoted_filename(self): filename = self.make_tempfile() # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('httpie > "%s"' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'http http://localhost\n') @pytest.mark.skipif(sys.platform == 'win32', reason="Windows doesn't use backslashes to escape") def test_httpie_redirect_write_escaped_filename(self): filename = self.make_tempfile() filename += r' copy' # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('httpie > %s' % filename.replace(' ', r'\ '), self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'http http://localhost\n') def test_httpie_redirect_write_with_args(self): filename = self.make_tempfile() # Write something first to make sure it's a full overwrite with open(filename, 'w') as f: f.write('hello world\n') execute('httpie post http://example.org name=john > %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'http POST http://example.org name=john\n') def test_httpie_redirect_append(self): filename = self.make_tempfile() # Write something first to make sure it's an append with open(filename, 'w') as f: f.write('hello world\n') execute('httpie >> %s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'hello world\nhttp http://localhost\n') def test_httpie_redirect_append_without_spaces(self): filename = self.make_tempfile() # Write something first to make sure it's an append with open(filename, 'w') as f: f.write('hello world\n') execute('httpie>>%s' % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'hello world\nhttp http://localhost\n') def test_httpie_redirect_append_quoted_filename(self): filename = self.make_tempfile() # Write something first to make sure it's an append with open(filename, 'w') as f: f.write('hello world\n') execute("httpie >> '%s'" % filename, self.context) with open(filename) as f: content = f.read() self.assertEqual(content, 'hello world\nhttp http://localhost\n') ================================================ FILE: tests/test_installation.py ================================================ """Test if http-prompt is installed correctly.""" import subprocess import pytest from subprocess import PIPE from .utils import get_http_prompt_path from http_prompt import __version__ def run_http_prompt(args): """Run http-prompt from terminal.""" bin_path = get_http_prompt_path() p = subprocess.Popen([bin_path] + args, stdin=PIPE, stdout=PIPE) return p.communicate() @pytest.mark.slow def test_help(): out, err = run_http_prompt(['--help']) assert out.startswith(b'Usage: http-prompt') @pytest.mark.slow def test_version(): out, err = run_http_prompt(['--version']) version = __version__ if hasattr(version, 'encode'): version = version.encode('ascii') assert out.rstrip() == version ================================================ FILE: tests/test_interaction.py ================================================ import os import sys import pexpect import pytest from .base import TempAppDirTestCase from .utils import get_http_prompt_path from http_prompt import config class TestInteraction(TempAppDirTestCase): def setUp(self): super(TestInteraction, self).setUp() # Use temporary directory as user config home. # Will restore it in tearDown(). self.orig_config_home = os.getenv('XDG_CONFIG_HOME') os.environ['XDG_CONFIG_HOME'] = self.temp_dir # Make sure pexpect uses the same terminal environment self.orig_term = os.getenv('TERM') os.environ['TERM'] = 'screen-256color' def tearDown(self): super(TestInteraction, self).tearDown() os.environ['XDG_CONFIG_HOME'] = self.orig_config_home if self.orig_term: os.environ['TERM'] = self.orig_term else: os.environ.pop('TERM', None) def write_config(self, content): config_path = config.get_user_config_path() with open(config_path, 'a') as f: f.write(content) @pytest.mark.skipif(sys.platform == 'win32', reason="pexpect doesn't work well on Windows") @pytest.mark.slow def test_interaction(self): bin_path = get_http_prompt_path() child = pexpect.spawn(bin_path, env=os.environ) # TODO: Test more interaction child.sendline('exit') child.expect_exact('Goodbye!', timeout=20) child.close() @pytest.mark.skipif(sys.platform == 'win32', reason="pexpect doesn't work well on Windows") @pytest.mark.slow def test_vi_mode(self): self.write_config('vi = True\n') bin_path = get_http_prompt_path() child = pexpect.spawn(bin_path, env=os.environ) child.expect_exact('http://localhost:8000>') # Enter 'htpie', switch to command mode (ESC), # move two chars left (hh), and insert (i) a 't' child.send('htpie') child.send('\x1b') child.sendline('hhit') child.expect_exact('http http://localhost:8000') # Enter 'exit' child.send('\x1b') child.send('i') child.sendline('exit') child.expect_exact('Goodbye!', timeout=20) child.close() ================================================ FILE: tests/test_lexer.py ================================================ import unittest from pygments.token import Keyword, String, Text, Error, Name, Operator from http_prompt.lexer import HttpPromptLexer class LexerTestCase(unittest.TestCase): def setUp(self): self.lexer = HttpPromptLexer() def get_tokens(self, text, filter_spaces=True): tokens = self.lexer.get_tokens(text) tokens = filter(lambda t: t[1], tokens) if filter_spaces: tokens = filter(lambda t: t[1].strip(), tokens) return list(tokens) class TestLexer_mutation(LexerTestCase): def test_querystring(self): self.assertEqual(self.get_tokens('foo==bar'), [ (Name, 'foo'), (Operator, '=='), (String, 'bar') ]) def test_body_param(self): self.assertEqual(self.get_tokens('foo=bar'), [ (Name, 'foo'), (Operator, '='), (String, 'bar') ]) def test_header(self): self.assertEqual(self.get_tokens('Accept:application/json'), [ (Name, 'Accept'), (Operator, ':'), (String, 'application/json') ]) def test_json_integer(self): self.assertEqual(self.get_tokens('number:=1'), [ (Name, 'number'), (Operator, ':='), (String, '1') ]) def test_json_boolean(self): self.assertEqual(self.get_tokens('enabled:=true'), [ (Name, 'enabled'), (Operator, ':='), (String, 'true') ]) def test_json_string(self): self.assertEqual(self.get_tokens('name:="foo bar"'), [ (Name, 'name'), (Operator, ':='), (Text, '"'), (String, 'foo bar'), (Text, '"') ]) def test_json_array(self): self.assertEqual(self.get_tokens('list:=[1,"two"]'), [ (Name, 'list'), (Operator, ':='), (String, '[1,"two"]'), ]) def test_json_array_quoted(self): self.assertEqual(self.get_tokens("""list:='[1,"two"]'"""), [ (Name, 'list'), (Operator, ':='), (Text, "'"), (String, '[1,"two"]'), (Text, "'"), ]) def test_json_object(self): self.assertEqual(self.get_tokens('object:={"id":123,"name":"foo"}'), [ (Name, 'object'), (Operator, ':='), (String, '{"id":123,"name":"foo"}'), ]) def test_json_object_quoted(self): self.assertEqual(self.get_tokens("""object:='{"id": 123}'"""), [ (Name, 'object'), (Operator, ':='), (Text, "'"), (String, '{"id": 123}'), (Text, "'") ]) def test_json_escaped_colon(self): self.assertEqual(self.get_tokens(r'where[id\:gt]:=2'), [ (Name, r'where[id\:gt]'), (Operator, ':='), (String, '2') ]) def test_body_param_escaped_equal(self): self.assertEqual(self.get_tokens(r'foo\=bar=hello'), [ (Name, r'foo\=bar'), (Operator, '='), (String, 'hello') ]) def test_parameter_name_including_http_method_name(self): self.assertEqual(self.get_tokens('heading==hello'), [ (Name, 'heading'), (Operator, '=='), (String, 'hello') ]) class TestLexer_cd(LexerTestCase): def test_simple(self): self.assertEqual(self.get_tokens('cd api/v1'), [ (Keyword, 'cd'), (String, 'api/v1') ]) def test_double_quoted(self): self.assertEqual(self.get_tokens('cd "api/v 1"'), [ (Keyword, 'cd'), (Text, '"'), (String, 'api/v 1'), (Text, '"') ]) def test_single_quoted(self): self.assertEqual(self.get_tokens("cd 'api/v 1'"), [ (Keyword, 'cd'), (Text, "'"), (String, 'api/v 1'), (Text, "'") ]) def test_escape(self): self.assertEqual(self.get_tokens(r"cd api/v\ 1"), [ (Keyword, 'cd'), (String, r'api/v\ 1') ]) def test_second_path(self): self.assertEqual(self.get_tokens(r"cd api v1"), [ (Keyword, 'cd'), (String, 'api'), (Error, 'v'), (Error, '1') ]) def test_leading_trailing_spaces(self): self.assertEqual(self.get_tokens(' cd api/v1 '), [ (Keyword, 'cd'), (String, 'api/v1') ]) class TestLexer_ls(LexerTestCase): def test_no_path(self): self.assertEqual(self.get_tokens('ls'), [ (Keyword, 'ls') ]) def test_path(self): self.assertEqual(self.get_tokens('ls api/v1'), [ (Keyword, 'ls'), (String, 'api/v1') ]) def test_second_path(self): self.assertEqual(self.get_tokens(r"ls api v1"), [ (Keyword, 'ls'), (String, 'api'), (Error, 'v'), (Error, '1') ]) def test_leading_trailing_spaces(self): self.assertEqual(self.get_tokens(' ls api/v1 '), [ (Keyword, 'ls'), (String, 'api/v1') ]) def test_redirect(self): self.assertEqual(self.get_tokens('ls api/v1 > endpoints.txt'), [ (Keyword, 'ls'), (String, 'api/v1'), (Operator, '>'), (String, 'endpoints.txt') ]) class TestLexer_env(LexerTestCase): def test_env_simple(self): self.assertEqual(self.get_tokens('env'), [ (Keyword, 'env'), ]) def test_env_with_spaces(self): self.assertEqual(self.get_tokens(' env '), [ (Keyword, 'env'), ]) def test_env_write(self): self.assertEqual(self.get_tokens('env > /tmp/file.txt'), [ (Keyword, 'env'), (Operator, '>'), (String, '/tmp/file.txt') ]) def test_env_append(self): self.assertEqual(self.get_tokens('env >> /tmp/file.txt'), [ (Keyword, 'env'), (Operator, '>>'), (String, '/tmp/file.txt') ]) def test_env_write_quoted_filename(self): self.assertEqual(self.get_tokens('env > "/tmp/my file.txt"'), [ (Keyword, 'env'), (Operator, '>'), (Text, '"'), (String, '/tmp/my file.txt'), (Text, '"') ]) def test_env_append_escaped_filename(self): self.assertEqual(self.get_tokens(r'env >> /tmp/my\ file.txt'), [ (Keyword, 'env'), (Operator, '>>'), (String, r'/tmp/my\ file.txt') ]) def test_env_pipe(self): self.assertEqual(self.get_tokens('env | grep name'), [ (Keyword, 'env'), (Operator, '|'), (Text, 'grep'), (Text, 'name') ]) class TestLexer_rm(LexerTestCase): def test_header(self): self.assertEqual(self.get_tokens('rm -h Accept'), [ (Keyword, 'rm'), (Name, '-h'), (String, 'Accept') ]) def test_header_escaped(self): self.assertEqual(self.get_tokens(r'rm -h Custom\ Header'), [ (Keyword, 'rm'), (Name, '-h'), (String, r'Custom\ Header') ]) def test_querystring(self): self.assertEqual(self.get_tokens('rm -q page'), [ (Keyword, 'rm'), (Name, '-q'), (String, 'page') ]) def test_querystring_double_quoted(self): self.assertEqual(self.get_tokens('rm -q "page size"'), [ (Keyword, 'rm'), (Name, '-q'), (Text, '"'), (String, 'page size'), (Text, '"') ]) def test_body_param(self): self.assertEqual(self.get_tokens('rm -b name'), [ (Keyword, 'rm'), (Name, '-b'), (String, 'name') ]) def test_body_param_single_quoted(self): self.assertEqual(self.get_tokens("rm -b 'first name'"), [ (Keyword, 'rm'), (Name, '-b'), (Text, "'"), (String, 'first name'), (Text, "'") ]) def test_option(self): self.assertEqual(self.get_tokens('rm -o --json'), [ (Keyword, 'rm'), (Name, '-o'), (String, '--json') ]) def test_reset(self): self.assertEqual(self.get_tokens('rm *'), [ (Keyword, 'rm'), (Name, '*') ]) def test_option_leading_trailing_spaces(self): self.assertEqual(self.get_tokens(' rm -o --json '), [ (Keyword, 'rm'), (Name, '-o'), (String, '--json') ]) def test_invalid_type(self): self.assertEqual(self.get_tokens('rm -a foo'), [ (Keyword, 'rm'), (Error, '-'), (Error, 'a'), (Error, 'f'), (Error, 'o'), (Error, 'o') ]) class TestLexer_help(LexerTestCase): def test_help_simple(self): self.assertEqual(self.get_tokens('help'), [ (Keyword, 'help') ]) def test_help_with_spaces(self): self.assertEqual(self.get_tokens(' help '), [ (Keyword, 'help') ]) class TestLexer_source(LexerTestCase): def test_source_simple_filename(self): self.assertEqual(self.get_tokens('source file.txt'), [ (Keyword, 'source'), (String, 'file.txt') ]) def test_source_with_spaces(self): self.assertEqual(self.get_tokens(' source file.txt '), [ (Keyword, 'source'), (String, 'file.txt') ]) def test_source_quoted_filename(self): self.assertEqual(self.get_tokens("source '/tmp/my file.txt'"), [ (Keyword, 'source'), (Text, "'"), (String, '/tmp/my file.txt'), (Text, "'") ]) def test_source_escaped_filename(self): self.assertEqual(self.get_tokens(r"source /tmp/my\ file.txt"), [ (Keyword, 'source'), (String, r'/tmp/my\ file.txt') ]) class TestLexer_exec(LexerTestCase): def test_exec_simple_filename(self): self.assertEqual(self.get_tokens('exec file.txt'), [ (Keyword, 'exec'), (String, 'file.txt') ]) def test_exec_with_spaces(self): self.assertEqual(self.get_tokens(' exec file.txt '), [ (Keyword, 'exec'), (String, 'file.txt') ]) def test_exec_quoted_filename(self): self.assertEqual(self.get_tokens("exec '/tmp/my file.txt'"), [ (Keyword, 'exec'), (Text, "'"), (String, '/tmp/my file.txt'), (Text, "'") ]) def test_exec_escaped_filename(self): self.assertEqual(self.get_tokens(r"exec /tmp/my\ file.txt"), [ (Keyword, 'exec'), (String, r'/tmp/my\ file.txt') ]) class TestLexer_exit(LexerTestCase): def test_exit_simple(self): self.assertEqual(self.get_tokens('exit'), [ (Keyword, 'exit') ]) def test_exit_with_spaces(self): self.assertEqual(self.get_tokens(' exit '), [ (Keyword, 'exit') ]) class TestLexerPreview(LexerTestCase): def test_httpie_without_action(self): cmd = 'httpie http://example.com name=jack' self.assertEqual(self.get_tokens(cmd), [ (Keyword, 'httpie'), (String, 'http://example.com'), (Name, 'name'), (Operator, '='), (String, 'jack') ]) def test_httpie_without_action_and_url(self): cmd = 'httpie name=jack Accept:*/*' self.assertEqual(self.get_tokens(cmd), [ (Keyword, 'httpie'), (Name, 'name'), (Operator, '='), (String, 'jack'), (Name, 'Accept'), (Operator, ':'), (String, '*/*') ]) def test_httpie_absolute_url(self): cmd = 'httpie post http://example.com name=jack' self.assertEqual(self.get_tokens(cmd), [ (Keyword, 'httpie'), (Keyword, 'post'), (String, 'http://example.com'), (Name, 'name'), (Operator, '='), (String, 'jack') ]) def test_httpie_option_first(self): self.assertEqual(self.get_tokens('httpie post --form name=jack'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, '--form'), (Name, 'name'), (Operator, '='), (String, 'jack') ]) def test_httpie_body_param_first(self): self.assertEqual(self.get_tokens('httpie post name=jack --form'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (String, 'jack'), (Name, '--form') ]) def test_httpie_options(self): self.assertEqual(self.get_tokens('httpie options test --body'), [ (Keyword, 'httpie'), (Keyword, 'options'), (String, 'test'), (Name, '--body') ]) def test_httpie_relative_path(self): tokens = self.get_tokens('httpie /api/test name==foo', filter_spaces=False) self.assertEqual(tokens, [ (Keyword, 'httpie'), (Text, ' '), (String, '/api/test'), (Text, ' '), (Name, 'name'), (Operator, '=='), (String, 'foo'), (Text, '\n') ]) class TestShellCode(LexerTestCase): def test_unquoted_querystring(self): self.assertEqual(self.get_tokens('`echo name`==john'), [ (Text, '`'), (Name.Builtin, 'echo'), (Text, 'name'), (Text, '`'), (Operator, '=='), (String, 'john') ]) self.assertEqual(self.get_tokens('name==`echo john`'), [ (Name, 'name'), (Operator, '=='), (Text, '`'), (Name.Builtin, 'echo'), (Text, 'john'), (Text, '`') ]) def test_unquoted_bodystring(self): self.assertEqual(self.get_tokens('`echo name`=john'), [ (Text, '`'), (Name.Builtin, 'echo'), (Text, 'name'), (Text, '`'), (Operator, '='), (String, 'john') ]) self.assertEqual(self.get_tokens('name=`echo john`'), [ (Name, 'name'), (Operator, '='), (Text, '`'), (Name.Builtin, 'echo'), (Text, 'john'), (Text, '`') ]) def test_header_option_value(self): self.assertEqual(self.get_tokens('Accept:`echo "application/json"`'), [ (Name, 'Accept'), (Operator, ':'), (Text, '`'), (Name.Builtin, 'echo'), (String.Double, '"application/json"'), (Text, '`'), ]) def test_httpie_body_param(self): self.assertEqual(self.get_tokens('httpie post name=`echo john`'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (Text, '`'), (Name.Builtin, 'echo'), (Text, 'john'), (Text, '`'), ]) def test_httpie_post_pipe(self): self.assertEqual(self.get_tokens('httpie post | tee "/tmp/test"'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Operator, '|'), (Text, 'tee'), (String.Double, '"/tmp/test"'), ]) def test_post_pipe(self): self.assertEqual(self.get_tokens('post | tee "/tmp/test"'), [ (Keyword, 'post'), (Operator, '|'), (Text, 'tee'), (String.Double, '"/tmp/test"'), ]) class TestLexerPreviewRedirection(LexerTestCase): def test_httpie_write(self): self.assertEqual(self.get_tokens('httpie > file.txt'), [ (Keyword, 'httpie'), (Operator, '>'), (String, 'file.txt') ]) def test_httpie_write_without_spaces(self): self.assertEqual(self.get_tokens('httpie>file.txt'), [ (Keyword, 'httpie'), (Operator, '>'), (String, 'file.txt') ]) def test_httpie_append(self): self.assertEqual(self.get_tokens('httpie >> file.txt'), [ (Keyword, 'httpie'), (Operator, '>>'), (String, 'file.txt') ]) def test_httpie_append_without_spaces(self): self.assertEqual(self.get_tokens('httpie>>file.txt'), [ (Keyword, 'httpie'), (Operator, '>>'), (String, 'file.txt') ]) def test_httpie_write_with_post_param(self): self.assertEqual(self.get_tokens('httpie post name=jack > file.txt'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (String, 'jack'), (Operator, '>'), (String, 'file.txt') ]) def test_httpie_append_with_post_param(self): self.assertEqual(self.get_tokens('httpie post name=doe >> file.txt'), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (String, 'doe'), (Operator, '>>'), (String, 'file.txt') ]) def test_httpie_write_quoted_filename(self): self.assertEqual(self.get_tokens("httpie > 'my file.txt'"), [ (Keyword, 'httpie'), (Operator, '>'), (Text, "'"), (String, 'my file.txt'), (Text, "'") ]) def test_httpie_append_quoted_filename(self): self.assertEqual(self.get_tokens('httpie >> "my file.txt"'), [ (Keyword, 'httpie'), (Operator, '>>'), (Text, '"'), (String, 'my file.txt'), (Text, '"') ]) def test_httpie_append_with_many_params(self): command = ("httpie post --auth user:pass --verify=no " "name='john doe' page==2 >> file.txt") self.assertEqual(self.get_tokens(command), [ (Keyword, 'httpie'), (Keyword, 'post'), (Name, '--auth'), (String, 'user:pass'), (Name, '--verify'), (Operator, '='), (String, 'no'), (Name, 'name'), (Operator, '='), (Text, "'"), (String, 'john doe'), (Text, "'"), (Name, 'page'), (Operator, '=='), (String, '2'), (Operator, '>>'), (String, 'file.txt') ]) def test_curl_write(self): self.assertEqual(self.get_tokens('curl > file.txt'), [ (Keyword, 'curl'), (Operator, '>'), (String, 'file.txt') ]) def test_curl_write_without_spaces(self): self.assertEqual(self.get_tokens('curl>file.txt'), [ (Keyword, 'curl'), (Operator, '>'), (String, 'file.txt') ]) def test_curl_append(self): self.assertEqual(self.get_tokens('curl >> file.txt'), [ (Keyword, 'curl'), (Operator, '>>'), (String, 'file.txt') ]) def test_curl_append_without_spaces(self): self.assertEqual(self.get_tokens('curl>>file.txt'), [ (Keyword, 'curl'), (Operator, '>>'), (String, 'file.txt') ]) def test_curl_write_with_post_param(self): self.assertEqual(self.get_tokens('curl post name=jack > file.txt'), [ (Keyword, 'curl'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (String, 'jack'), (Operator, '>'), (String, 'file.txt') ]) def test_curl_append_with_post_param(self): self.assertEqual(self.get_tokens('curl post name=doe >> file.txt'), [ (Keyword, 'curl'), (Keyword, 'post'), (Name, 'name'), (Operator, '='), (String, 'doe'), (Operator, '>>'), (String, 'file.txt') ]) def test_curl_write_quoted_filename(self): self.assertEqual(self.get_tokens("curl > 'my file.txt'"), [ (Keyword, 'curl'), (Operator, '>'), (Text, "'"), (String, 'my file.txt'), (Text, "'") ]) def test_curl_append_quoted_filename(self): self.assertEqual(self.get_tokens('curl >> "my file.txt"'), [ (Keyword, 'curl'), (Operator, '>>'), (Text, '"'), (String, 'my file.txt'), (Text, '"') ]) def test_curl_append_with_many_params(self): command = ("curl post --auth user:pass --verify=no " "name='john doe' page==2 >> file.txt") self.assertEqual(self.get_tokens(command), [ (Keyword, 'curl'), (Keyword, 'post'), (Name, '--auth'), (String, 'user:pass'), (Name, '--verify'), (Operator, '='), (String, 'no'), (Name, 'name'), (Operator, '='), (Text, "'"), (String, 'john doe'), (Text, "'"), (Name, 'page'), (Operator, '=='), (String, '2'), (Operator, '>>'), (String, 'file.txt') ]) class TestLexerAction(LexerTestCase): def test_get(self): self.assertEqual(self.get_tokens('get'), [ (Keyword, 'get') ]) def test_post_with_spaces(self): self.assertEqual(self.get_tokens(' post '), [ (Keyword, 'post') ]) def test_capital_head(self): self.assertEqual(self.get_tokens('HEAD'), [ (Keyword, 'HEAD') ]) def test_delete_random_capitals(self): self.assertEqual(self.get_tokens('dElETe'), [ (Keyword, 'dElETe') ]) def test_patch(self): self.assertEqual(self.get_tokens('patch'), [ (Keyword, 'patch') ]) def test_get_with_querystring_params(self): command = 'get page==10 id==200' self.assertEqual(self.get_tokens(command), [ (Keyword, 'get'), (Name, 'page'), (Operator, '=='), (String, '10'), (Name, 'id'), (Operator, '=='), (String, '200') ]) def test_capital_get_with_querystring_params(self): command = 'GET page==10 id==200' self.assertEqual(self.get_tokens(command), [ (Keyword, 'GET'), (Name, 'page'), (Operator, '=='), (String, '10'), (Name, 'id'), (Operator, '=='), (String, '200') ]) def test_post_with_body_params(self): command = 'post name="john doe" username=john' self.assertEqual(self.get_tokens(command), [ (Keyword, 'post'), (Name, 'name'), (Operator, '='), (Text, '"'), (String, 'john doe'), (Text, '"'), (Name, 'username'), (Operator, '='), (String, 'john') ]) def test_post_with_spaces_and_body_params(self): command = ' post name="john doe" username=john ' self.assertEqual(self.get_tokens(command), [ (Keyword, 'post'), (Name, 'name'), (Operator, '='), (Text, '"'), (String, 'john doe'), (Text, '"'), (Name, 'username'), (Operator, '='), (String, 'john') ]) def test_options(self): self.assertEqual(self.get_tokens('options'), [ (Keyword, 'options') ]) def test_post_relative_path(self): tokens = self.get_tokens('post /api/test name=foo', filter_spaces=False) self.assertEqual(tokens, [ (Keyword, 'post'), (Text, ' '), (String, '/api/test'), (Text, ' '), (Name, 'name'), (Operator, '='), (String, 'foo'), (Text, '\n') ]) class TestLexerActionRedirection(LexerTestCase): def test_get_write(self): self.assertEqual(self.get_tokens('get > file.txt'), [ (Keyword, 'get'), (Operator, '>'), (String, 'file.txt') ]) def test_get_write_quoted_filename(self): self.assertEqual(self.get_tokens('get > "/tmp/my file.txt"'), [ (Keyword, 'get'), (Operator, '>'), (Text, '"'), (String, '/tmp/my file.txt'), (Text, '"') ]) def test_get_append(self): self.assertEqual(self.get_tokens('get >> file.txt'), [ (Keyword, 'get'), (Operator, '>>'), (String, 'file.txt') ]) def test_get_append_escaped_filename(self): self.assertEqual(self.get_tokens(r'get >> /tmp/my\ file.txt'), [ (Keyword, 'get'), (Operator, '>>'), (String, r'/tmp/my\ file.txt') ]) def test_post_append_with_spaces(self): self.assertEqual(self.get_tokens(' post >> file.txt'), [ (Keyword, 'post'), (Operator, '>>'), (String, 'file.txt') ]) def test_capital_head_write(self): self.assertEqual(self.get_tokens('HEAD > file.txt'), [ (Keyword, 'HEAD'), (Operator, '>'), (String, 'file.txt') ]) def test_get_append_with_querystring_params(self): command = 'get page==10 id==200 >> /tmp/file.txt' self.assertEqual(self.get_tokens(command), [ (Keyword, 'get'), (Name, 'page'), (Operator, '=='), (String, '10'), (Name, 'id'), (Operator, '=='), (String, '200'), (Operator, '>>'), (String, '/tmp/file.txt') ]) def test_post_write_escaped_filename_with_body_params(self): command = r'post name="john doe" username=john > /tmp/my\ file.txt' self.assertEqual(self.get_tokens(command), [ (Keyword, 'post'), (Name, 'name'), (Operator, '='), (Text, '"'), (String, 'john doe'), (Text, '"'), (Name, 'username'), (Operator, '='), (String, 'john'), (Operator, '>'), (String, r'/tmp/my\ file.txt') ]) def test_post_append_with_spaces_and_body_params(self): command = ' post name="john doe" username=john >> /tmp/file.txt ' self.assertEqual(self.get_tokens(command), [ (Keyword, 'post'), (Name, 'name'), (Operator, '='), (Text, '"'), (String, 'john doe'), (Text, '"'), (Name, 'username'), (Operator, '='), (String, 'john'), (Operator, '>>'), (String, '/tmp/file.txt') ]) ================================================ FILE: tests/test_tree.py ================================================ import unittest from http_prompt.tree import Node class TestNode(unittest.TestCase): def setUp(self): # Make a tree like this: # root # a h # b d i n # c f e g k o # l m p self.root = Node('root') self.root.add_path('a', 'b', 'c') self.root.add_path('a', 'b', 'f') self.root.add_path('a', 'd', 'e') self.root.add_path('a', 'd', 'g') self.root.add_path('h', 'i', 'k', 'l') self.root.add_path('h', 'i', 'k', 'm') self.root.add_path('h', 'i', 'k', 'p') self.root.add_path('h', 'n', 'o') def test_illegal_name(self): self.assertRaises(ValueError, Node, '.') self.assertRaises(ValueError, Node, '..') def test_str(self): node = Node('my node') self.assertEqual(str(node), 'my node') def test_cmp_same_type(self): a = Node('a', data={'type': 'dir'}) b = Node('b', data={'type': 'dir'}) self.assertTrue(a < b) def test_cmp_different_type(self): a = Node('a', data={'type': 'file'}) b = Node('b', data={'type': 'dir'}) self.assertTrue(b < a) def test_eq(self): a = Node('a', data={'type': 'file'}) b = Node('b', data={'type': 'dir'}) self.assertNotEqual(a, b) a = Node('a', data={'type': 'file'}) b = Node('a', data={'type': 'file'}) self.assertEqual(a, b) def test_add_path_and_find_child(self): # Level 1 (root) self.assertEqual(set(c.name for c in self.root.children), set('ah')) # Level 2 node_a = self.root.find_child('a') node_h = self.root.find_child('h') self.assertEqual(set(c.name for c in node_a.children), set('bd')) self.assertEqual(set(c.name for c in node_h.children), set('in')) # Level 3 node_b = node_a.find_child('b') node_i = node_h.find_child('i') self.assertEqual(set(c.name for c in node_b.children), set('cf')) self.assertEqual(set(c.name for c in node_i.children), set('k')) # Level 4 node_c = node_b.find_child('c') node_k = node_i.find_child('k') self.assertEqual(set(c.name for c in node_c.children), set()) self.assertEqual(set(c.name for c in node_k.children), set('lmp')) # Return None if child can't be found self.assertFalse(node_c.find_child('x')) def test_find_child_wildcard(self): root = Node('root') root.add_path('a') root.add_path('{b}') root.add_path('c') self.assertEqual(root.find_child('a').name, 'a') self.assertEqual(root.find_child('c').name, 'c') self.assertEqual(root.find_child('x').name, '{b}') self.assertFalse(root.find_child('x', wildcard=False)) def test_ls(self): self.assertEqual([n.name for n in self.root.ls('a')], list('bd')) self.assertEqual([n.name for n in self.root.ls('a', 'b')], list('cf')) self.assertEqual([n.name for n in self.root.ls('a', 'b', 'c')], []) self.assertEqual([n.name for n in self.root.ls('h', 'i', 'k')], list('lmp')) def test_ls_root(self): self.assertEqual([n.name for n in self.root.ls()], list('ah')) def test_ls_non_existing(self): self.assertEqual([n.name for n in self.root.ls('x')], []) self.assertEqual([n.name for n in self.root.ls('a', 'b', 'x')], []) def test_ls_parent(self): self.assertEqual([n.name for n in self.root.ls('..')], list('ah')) self.assertEqual([n.name for n in self.root.ls('..', '..', '..')], list('ah')) self.assertEqual([n.name for n in self.root.ls('..', '..', 'h')], list('in')) self.assertEqual( [n.name for n in self.root.ls('..', '..', 'h', '..', 'a')], list('bd')) def test_ls_dot(self): self.assertEqual([n.name for n in self.root.ls('.')], list('ah')) self.assertEqual([n.name for n in self.root.ls('.', '.', '.')], list('ah')) self.assertEqual([n.name for n in self.root.ls('.', 'a', 'b')], list('cf')) self.assertEqual([n.name for n in self.root.ls('.', 'h', '.')], list('in')) self.assertEqual( [n.name for n in self.root.ls('.', 'h', '.', '.', 'n')], ['o']) def test_ls_sort_by_types(self): self.root.add_path('q', 'r') self.root.add_path('q', 's', node_type='file') self.root.add_path('q', 't', node_type='file') self.root.add_path('q', 'u') self.root.add_path('q', 'v', node_type='file') self.assertEqual([n.name for n in self.root.ls('q')], list('rustv')) ================================================ FILE: tests/test_utils.py ================================================ from http_prompt import utils def test_colformat_zero_items(): assert list(utils.colformat([], terminal_width=80)) == [] def test_colformat_one_item(): assert list(utils.colformat(['hello'], terminal_width=80)) == ['hello'] def test_colformat_single_line(): items = ['hello', 'world', 'foo', 'bar'] assert list(utils.colformat(items, terminal_width=80)) == [ 'hello world foo bar' ] def test_colformat_single_column(): items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt'] assert list(utils.colformat(items, terminal_width=10)) == [ 'chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt' ] def test_colformat_multi_columns_no_remainder(): items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt'] assert list(utils.colformat(items, terminal_width=50)) == [ 'chap1.txt chap4.txt chap7.txt chap10.txt', 'chap2.txt chap5.txt chap8.txt chap11.txt', 'chap3.txt chap6.txt chap9.txt chap12.txt' ] def test_colformat_multi_columns_remainder_1(): items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt', 'chap13.txt'] assert list(utils.colformat(items, terminal_width=50)) == [ 'chap1.txt chap5.txt chap9.txt chap13.txt', 'chap2.txt chap6.txt chap10.txt', 'chap3.txt chap7.txt chap11.txt', 'chap4.txt chap8.txt chap12.txt' ] def test_colformat_multi_columns_remainder_2(): items = ['chap1.txt', 'chap2.txt', 'chap3.txt', 'chap4.txt', 'chap5.txt', 'chap6.txt', 'chap7.txt', 'chap8.txt', 'chap9.txt', 'chap10.txt', 'chap11.txt', 'chap12.txt', 'chap13.txt', 'chap14.txt'] assert list(utils.colformat(items, terminal_width=50)) == [ 'chap1.txt chap5.txt chap9.txt chap13.txt', 'chap2.txt chap6.txt chap10.txt chap14.txt', 'chap3.txt chap7.txt chap11.txt', 'chap4.txt chap8.txt chap12.txt' ] def test_colformat_wider_than_terminal(): items = ['a very long long name', '1111 2222 3333 4444 5555'] assert list(utils.colformat(items, terminal_width=10)) == [ 'a very long long name', '1111 2222 3333 4444 5555' ] def test_colformat_long_short_mixed(): items = ['a', '1122334455667788', 'hello world', 'foo bar', 'b', '8877665544332211', 'abcd', 'yeah'] assert list(utils.colformat(items, terminal_width=50)) == [ 'a foo bar abcd', '1122334455667788 b yeah', 'hello world 8877665544332211' ] def test_colformat_github_top_endpoints(): items = ['emojis', 'events', 'feeds', 'gists', 'gitignore', 'issues', 'legacy', 'markdown', 'meta', 'networks', 'notifications', 'orgs', 'rate_limit', 'repos', 'repositories', 'search', 'teams', 'user', 'users'] assert list(utils.colformat(items, terminal_width=136)) == [ 'emojis gists legacy networks rate_limit'' search users', # noqa 'events gitignore markdown notifications repos teams', # noqa 'feeds issues meta orgs repositories user' # noqa ] ================================================ FILE: tests/test_xdg.py ================================================ import os import stat import sys from .base import TempAppDirTestCase from http_prompt import xdg class TestXDG(TempAppDirTestCase): def test_get_app_data_home(self): path = xdg.get_data_dir() expected_path = os.path.join(os.environ[self.homes['data']], 'http-prompt') self.assertEqual(path, expected_path) self.assertTrue(os.path.exists(path)) if sys.platform != 'win32': # Make sure permission for the directory is 700 mask = stat.S_IMODE(os.stat(path).st_mode) self.assertTrue(mask & stat.S_IRWXU) self.assertFalse(mask & stat.S_IRWXG) self.assertFalse(mask & stat.S_IRWXO) def test_get_app_config_home(self): path = xdg.get_config_dir() expected_path = os.path.join(os.environ[self.homes['config']], 'http-prompt') self.assertEqual(path, expected_path) self.assertTrue(os.path.exists(path)) if sys.platform != 'win32': # Make sure permission for the directory is 700 mask = stat.S_IMODE(os.stat(path).st_mode) self.assertTrue(mask & stat.S_IRWXU) self.assertFalse(mask & stat.S_IRWXG) self.assertFalse(mask & stat.S_IRWXO) def test_get_resource_data_dir(self): path = xdg.get_data_dir('something') expected_path = os.path.join( os.environ[self.homes['data']], 'http-prompt', 'something') self.assertEqual(path, expected_path) self.assertTrue(os.path.exists(path)) # Make sure we can write a file to the directory with open(os.path.join(path, 'test'), 'wb') as f: f.write(b'hello') def test_get_resource_config_dir(self): path = xdg.get_config_dir('something') expected_path = os.path.join( os.environ[self.homes['config']], 'http-prompt', 'something') self.assertEqual(path, expected_path) self.assertTrue(os.path.exists(path)) # Make sure we can write a file to the directory with open(os.path.join(path, 'test'), 'wb') as f: f.write(b'hello') ================================================ FILE: tests/utils.py ================================================ import os import sys def get_http_prompt_path(): """Get the path to http-prompt executable.""" python_dir = os.path.dirname(sys.executable) bin_name = 'http-prompt' if sys.platform == 'win32': bin_name += '.exe' paths = [ os.path.join(python_dir, bin_name), os.path.join(python_dir, 'Scripts', bin_name), # Windows '/usr/bin/http-prompt' # Homebrew installation ] for path in paths: if os.path.exists(path): return path raise OSError("could not locate http-prompt executable, " "Python directory: %s" % python_dir) ================================================ FILE: tox.ini ================================================ # Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py3{6,7,8,9,10}, pypy3 [testenv] commands = pytest deps = -rrequirements-test.txt setenv = LC_ALL = en_US.utf-8 LANG = en_US.utf-8 [pytest] testpaths = tests addopts = --cov-config .coveragerc --cov http_prompt