├── quaeso
├── __init__.py
├── writer.py
├── reader.py
├── __main__.py
├── colorizer.py
├── formatter.py
└── yeet.py
├── tests
├── __init__.py
├── test_formatter.py
├── test_reader.py
└── test_writer.py
├── MANIFEST.in
├── requirements.txt
├── LICENSE
├── setup.py
├── .gitignore
└── README.md
/quaeso/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests==2.*
2 | PyYAML==6.*
3 | lxml==4.*
4 | Pygments==2.*
5 |
--------------------------------------------------------------------------------
/quaeso/writer.py:
--------------------------------------------------------------------------------
1 | from typing import Union, Protocol
2 |
3 |
4 | class Writeable(Protocol):
5 | def write(self, content: Union[str, bytes]):
6 | ...
7 |
8 |
9 | def write(content: Union[str, bytes], to: Writeable, formatter=None, colorizer=None):
10 | if isinstance(content, str):
11 | content = formatter(content) if formatter else content
12 | content = colorizer(content) if colorizer else content
13 | content = "\n" + content + "\n"
14 | to.write(content)
15 |
--------------------------------------------------------------------------------
/quaeso/reader.py:
--------------------------------------------------------------------------------
1 | import json
2 | import yaml
3 | from functools import partial
4 |
5 |
6 | class UnsupportedFileTypeException(Exception): pass
7 |
8 |
9 | def read_request_file(filepath) -> dict:
10 | if filepath.endswith(".yml") or filepath.endswith(".yaml"):
11 | loader = partial(yaml.load, Loader=yaml.FullLoader)
12 | elif filepath.endswith(".json"):
13 | loader = json.load
14 | else:
15 | raise UnsupportedFileTypeException(filepath)
16 |
17 | with open(filepath, "r") as f:
18 | file_data = loader(f)
19 |
20 | return file_data
21 |
--------------------------------------------------------------------------------
/quaeso/__main__.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | from quaeso.yeet import Yeeter
4 |
5 |
6 | def main():
7 | args = parse_args()
8 | yeeter = Yeeter(args.colorize)
9 | yeeter.yeet(args.filepath)
10 |
11 |
12 | def parse_args():
13 | ap = argparse.ArgumentParser(allow_abbrev=False)
14 | ap.add_argument(
15 | "-f",
16 | "--filepath",
17 | type=str,
18 | required=True,
19 | help="Request filepath in .json or .yaml or .yml format",
20 | )
21 | ap.add_argument(
22 | "-c",
23 | "--colorize",
24 | action='store_true',
25 | help="colorize stdout and stderr"
26 | )
27 | return ap.parse_args()
28 |
--------------------------------------------------------------------------------
/quaeso/colorizer.py:
--------------------------------------------------------------------------------
1 | from pygments import highlight
2 | from pygments.lexers.data import JsonLexer, YamlLexer
3 | from pygments.lexers.html import HtmlLexer, XmlLexer
4 | from pygments.formatters.terminal256 import TerminalTrueColorFormatter
5 | from pygments.formatters.terminal import TerminalFormatter
6 |
7 |
8 | def colorize_metadata_string(text) -> str:
9 | colorized = highlight(text, YamlLexer(), TerminalTrueColorFormatter())
10 | return colorized
11 |
12 |
13 | def colorize_json_string(text) -> str:
14 | colorized = highlight(text, JsonLexer(), TerminalFormatter())
15 | return colorized
16 |
17 |
18 | def colorize_xml_string(text) -> str:
19 | colorized = highlight(text, XmlLexer(), TerminalFormatter())
20 | return colorized
21 |
22 |
23 | def colorize_html_string(text) -> str:
24 | colorized = highlight(text, HtmlLexer(), TerminalFormatter())
25 | return colorized
26 |
27 |
28 | def no_colorizer(text) -> str:
29 | return text
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 zahash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # pip3 install setuptools twine wheel
2 | # python3 setup.py sdist bdist_wheel
3 | # twine upload dist/*
4 |
5 |
6 | import pathlib
7 | from setuptools import setup
8 |
9 | # The directory containing this file
10 | HERE = pathlib.Path(__file__).parent
11 |
12 | README = (HERE / "README.md").read_text()
13 |
14 | with (HERE / "requirements.txt").open() as f:
15 | requirements = f.read().splitlines()
16 |
17 | setup(
18 | name="quaeso",
19 | version="0.1.2",
20 | description="python cli program to send requests",
21 | long_description=README,
22 | long_description_content_type="text/markdown",
23 | url="https://github.com/zahash/quaeso",
24 | author="zahash",
25 | author_email="zahash.z@gmail.com",
26 | license="MIT",
27 | entry_points={
28 | 'console_scripts': [
29 | 'quaeso = quaeso.__main__:main',
30 | ],
31 | },
32 | python_requires='>=3.8',
33 | classifiers=[
34 | "License :: OSI Approved :: MIT License",
35 | "Programming Language :: Python :: 3",
36 | ],
37 | packages=["quaeso"],
38 | include_package_data=True,
39 | install_requires=requirements,
40 | )
41 |
--------------------------------------------------------------------------------
/quaeso/formatter.py:
--------------------------------------------------------------------------------
1 | import json
2 | from lxml import etree, html
3 |
4 |
5 | def format_metadata(text: str) -> str:
6 | json_obj: dict = json.loads(text)
7 | lines = [
8 | f"{key}: {value}"
9 | for key, value in sorted(json_obj.items(), key=_by_key_lower)
10 | ]
11 | formatted = "\n".join(lines)
12 | return formatted
13 |
14 |
15 | def format_json_string(text: str, indent=4) -> str:
16 | json_obj: dict = json.loads(text)
17 | formatted = json.dumps(json_obj, indent=indent)
18 | return formatted
19 |
20 |
21 | def format_xml_string(text: str, indent=2) -> str:
22 | xml_obj = etree.fromstring(text)
23 | etree.indent(xml_obj, space=" " * indent)
24 | formatted = etree.tostring(xml_obj, encoding="unicode")
25 | return formatted
26 |
27 |
28 | def format_html_string(text: str, indent=2) -> str:
29 | html_obj = html.fromstring(text)
30 | etree.indent(html_obj, space=" " * indent)
31 | formatted = html.tostring(html_obj, encoding="unicode")
32 | return formatted
33 |
34 |
35 | def no_format(text: str):
36 | return text
37 |
38 |
39 | def _by_key_lower(dict_item: tuple): key, val = dict_item; return key.lower()
40 |
--------------------------------------------------------------------------------
/tests/test_formatter.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from quaeso import formatter
3 |
4 |
5 | class TestFormatter(unittest.TestCase):
6 | def test_format_metadata(self):
7 | metadata = '{"const1": "const2"}'
8 | formatted = formatter.format_metadata(metadata)
9 | self.assertEqual(formatted, 'const1: const2')
10 |
11 | def test_format_json_string(self):
12 | json_string = '{"const1": "const2"}'
13 | formatted = formatter.format_json_string(json_string, indent=3)
14 | self.assertEqual(formatted, (
15 | '{\n'
16 | ' "const1": "const2"\n'
17 | '}'
18 | ))
19 |
20 | def test_format_xml_string(self):
21 | xml_string = '
const1
' 31 | formatted = formatter.format_html_string(html_string, indent=3) 32 | self.assertEqual(formatted, ( 33 | '\n' 34 | ' \n' 35 | 'const1
\n' 36 | ' \n' 37 | '' 38 | )) 39 | -------------------------------------------------------------------------------- /tests/test_reader.py: -------------------------------------------------------------------------------- 1 | import json 2 | import yaml 3 | import tempfile 4 | import unittest 5 | 6 | from quaeso import reader 7 | 8 | 9 | class TestReader(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls) -> None: 12 | cls.file_data = {"const1": "const2"} 13 | 14 | def test_read_request_file_with_json_file(self): 15 | _, fpath = tempfile.mkstemp(suffix=".json") 16 | with open(fpath, "w") as f: 17 | json.dump(self.file_data, f) 18 | self.assertEqual(reader.read_request_file(fpath), self.file_data) 19 | 20 | def test_read_request_file_with_yml_file(self): 21 | _, fpath = tempfile.mkstemp(suffix=".yml") 22 | with open(fpath, "w") as f: 23 | yaml.dump(self.file_data, f) 24 | self.assertEqual(reader.read_request_file(fpath), self.file_data) 25 | 26 | def test_read_request_file_with_yaml_file(self): 27 | _, fpath = tempfile.mkstemp(suffix=".yaml") 28 | with open(fpath, "w") as f: 29 | yaml.dump(self.file_data, f) 30 | self.assertEqual(reader.read_request_file(fpath), self.file_data) 31 | 32 | def test_read_request_file_with_invalid_file(self): 33 | _, fpath = tempfile.mkstemp(suffix=".asdf") 34 | with open(fpath, "w") as f: 35 | yaml.dump(self.file_data, f) 36 | with self.assertRaises(reader.UnsupportedFileTypeException): 37 | reader.read_request_file(fpath) 38 | -------------------------------------------------------------------------------- /quaeso/yeet.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from collections import namedtuple 4 | 5 | from requests.api import request 6 | from requests import Response 7 | from quaeso import formatter 8 | 9 | from quaeso.reader import read_request_file 10 | from quaeso.writer import write 11 | from quaeso import colorizer 12 | 13 | 14 | class Yeeter: 15 | Visual = namedtuple("Visual", ["formatter", "colorizer"]) 16 | 17 | def __init__(self, colorize: bool = True): 18 | sys.stdout.reconfigure(encoding='utf-8') 19 | 20 | # if output stays on terminal then isatty() returns True 21 | # if output is redirected to file then isatty() returns False 22 | self.colorize_stdout: bool = colorize and sys.stdout.isatty() 23 | self.colorize_stderr: bool = colorize and sys.stderr.isatty() 24 | 25 | self.content_type_visual = {} 26 | for content_type, formatter_fn, colorizer_fn in [ 27 | ("application/json", formatter.format_json_string, colorizer.colorize_json_string), 28 | ("text/xml", formatter.format_xml_string, colorizer.colorize_xml_string), 29 | ("text/html", formatter.format_html_string, colorizer.colorize_html_string) 30 | ]: 31 | self.content_type_visual[content_type] = self.Visual(formatter_fn, colorizer_fn) 32 | 33 | def yeet(self, request_filepath): 34 | request_data: dict = read_request_file(request_filepath) 35 | response: Response = request(**request_data) 36 | 37 | metadata_text = json.dumps(dict(**response.headers, **{"status": response.status_code, "url": response.url})) 38 | write(metadata_text, sys.stderr, 39 | formatter=formatter.format_metadata, 40 | colorizer=colorizer.colorize_metadata_string if self.colorize_stderr else None) 41 | 42 | content_type: str = self._get_content_type(response) 43 | if content_type in self.content_type_visual.keys(): 44 | write(response.content.decode('utf-8'), sys.stdout, 45 | formatter=self.content_type_visual[content_type].formatter, 46 | colorizer=self.content_type_visual[content_type].colorizer if self.colorize_stdout else None) 47 | else: 48 | write(response.content, sys.stdout.buffer, formatter=None, colorizer=None) 49 | 50 | @staticmethod 51 | def _get_content_type(res: Response) -> str: 52 | content_type, *_ = res.headers["content-type"].split(";") 53 | return content_type.strip().lower() 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # jetbrains 132 | .idea/ 133 | -------------------------------------------------------------------------------- /tests/test_writer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from typing import Union 3 | from quaeso import writer 4 | 5 | 6 | class FakeWriteable: 7 | def __init__(self): 8 | self.written_content = None 9 | 10 | def write(self, content: Union[str, bytes]): 11 | self.written_content = content 12 | 13 | 14 | def fake_text_formatter(content: str): 15 | return "formatted_" + content 16 | 17 | 18 | def fake_text_colorizer(content: str): 19 | return "colorized_" + content 20 | 21 | 22 | def fake_bytes_formatter(content: bytes): 23 | return b"formatted_" + content 24 | 25 | 26 | def fake_bytes_colorizer(content: bytes): 27 | return b"colorized_" + content 28 | 29 | 30 | class TestWriter(unittest.TestCase): 31 | def setUp(self) -> None: 32 | self.fake_writeable = FakeWriteable() 33 | 34 | def test_write_text_noformatter_nocolorizer(self): 35 | content = "const1" 36 | writer.write(content, self.fake_writeable, formatter=None, colorizer=None) 37 | self.assertEqual(self.fake_writeable.written_content.strip().strip(), content) 38 | 39 | def test_write_text_withformatter_nocolorizer(self): 40 | content = "const1" 41 | writer.write(content, self.fake_writeable, formatter=fake_text_formatter, colorizer=None) 42 | self.assertEqual(self.fake_writeable.written_content.strip(), fake_text_formatter(content)) 43 | 44 | def test_write_text_noformatter_withcolorizer(self): 45 | content = "const1" 46 | writer.write(content, self.fake_writeable, formatter=None, colorizer=fake_text_colorizer) 47 | self.assertEqual(self.fake_writeable.written_content.strip(), fake_text_colorizer(content)) 48 | 49 | def test_write_text_withformatter_withcolorizer(self): 50 | content = "const1" 51 | writer.write(content, self.fake_writeable, formatter=fake_text_formatter, colorizer=fake_text_colorizer) 52 | self.assertEqual(self.fake_writeable.written_content.strip(), fake_text_colorizer(fake_text_formatter(content))) 53 | 54 | def test_write_bytes_noformatter_nocolorizer(self): 55 | content = b"const1" 56 | writer.write(content, self.fake_writeable, formatter=None, colorizer=None) 57 | self.assertEqual(self.fake_writeable.written_content.strip(), content) 58 | 59 | def test_write_bytes_withformatter_nocolorizer(self): 60 | content = b"const1" 61 | writer.write(content, self.fake_writeable, formatter=fake_bytes_formatter, colorizer=None) 62 | self.assertEqual(self.fake_writeable.written_content.strip(), content) 63 | 64 | def test_write_bytes_noformatter_withcolorizer(self): 65 | content = b"const1" 66 | writer.write(content, self.fake_writeable, formatter=None, colorizer=fake_bytes_colorizer) 67 | self.assertEqual(self.fake_writeable.written_content.strip(), content) 68 | 69 | def test_write_bytes_withformatter_withcolorizer(self): 70 | content = b"const1" 71 | writer.write(content, self.fake_writeable, formatter=fake_bytes_formatter, colorizer=fake_bytes_colorizer) 72 | self.assertEqual(self.fake_writeable.written_content.strip(), content) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |3 | ██████╗ ██╗ ██╗ █████╗ ███████╗███████╗ ██████╗ 4 | ██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝██╔═══██╗ 5 | ██║ ██║██║ ██║███████║█████╗ ███████╗██║ ██║ 6 | ██║▄▄ ██║██║ ██║██╔══██║██╔══╝ ╚════██║██║ ██║ 7 | ╚██████╔╝╚██████╔╝██║ ██║███████╗███████║╚██████╔╝ 8 | ╚══▀▀═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ 9 | --------------------------------------------------- 10 | python cli program to send requests 11 |12 | 13 | [](https://pypi.org/project/quaeso/) 14 | [](https://opensource.org/licenses/MIT) 15 | 16 |