{ "info": { "author": "Mariusz Kupidura", "author_email": "f4wkes@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Shells" ], "description": "# riposte\n[![Build Status](https://travis-ci.org/fwkz/riposte.svg?branch=master)](https://travis-ci.org/fwkz/riposte)\n[![License](https://img.shields.io/pypi/l/riposte.svg)](https://github.com/fwkz/riposte/blob/master/LICENSE)\n[![Version](https://img.shields.io/pypi/v/riposte.svg)](https://pypi.org/project/riposte/)\n[![Python](https://img.shields.io/pypi/pyversions/riposte.svg)](https://pypi.org/project/riposte/)\n[![Code Style](https://img.shields.io/badge/codestyle-black-black.svg)](https://github.com/ambv/black)\n\n_Riposte_ allows you to easily wrap your application inside a tailored \ninteractive shell. Common chores regarding building REPLs was factored out and \nbeing taken care of so you can really focus on specific domain logic of your \napplication.\n\nThe motivation for building _Riposte_ coming from many sleepless nights of \nhandling numerous tricky cases regarding REPLs during \n[routersploit](https://github.com/threat9/routersploit) development. Like \nevery other project it began very innocently but after a while, when the project \ngot some real traction and code base was rapidly growing, shell logic started \nto intertwine with domain logic making things less and less readable and \ncontributor friendly.\n\nMoreover, to our surprise, people started to fork \n[routersploit](https://github.com/threat9/routersploit) not because they were \ninterested in the security of embedded devices but simply because they want to \nleverage our interactive shell logic and build their own tools using similar \nconcept. All these years they must have said: _\"There must be a better way!\"_ \nand they were completely right, the better way is called _Riposte_.\n\n## Table of contents\n* [Getting started](#getting-started)\n * [Installing](#installing)\n * [Example usage](#example-usage)\n* [Manual](#manual)\n * [Command](#command)\n * [Completer](#completer)\n * [Guides](#guides)\n * [Printing](#printing)\n * [History](#history)\n * [Prompt](#Prompt)\n * [Banner](#Banner)\n * [Inline command execution](#Inline-command-execution)\n * [CLI](#CLI)\n * [Input streams](#input-streams)\n* [Project status](#project-status)\n* [Contributing](#contributing)\n* [Versioning](#versioning)\n* [License](#license)\n* [Acknowledgments](#acknowledgments)\n\n## Getting Started\n### Installing\nThe package is available on PyPI so please use \n[pip](https://pip.pypa.io/en/stable/quickstart/) to install it: \n```bash\npip install riposte\n```\nRiposte supports Python 3.6 and newer.\n\n### Example usage\n```python\nfrom riposte import Riposte\n\ncalculator = Riposte(prompt=\"calc:~$ \")\n\nMEMORY = []\n\n\n@calculator.command(\"add\")\ndef add(x: int, y: int):\n result = f\"{x} + {y} = {x + y}\"\n MEMORY.append(result)\n calculator.success(result)\n\n\n@calculator.command(\"multiply\")\ndef multiply(x: int, y: int):\n result = f\"{x} * {y} = {x * y}\"\n MEMORY.append(result)\n calculator.success(result)\n\n\n@calculator.command(\"memory\")\ndef memory():\n for entry in MEMORY:\n calculator.print(entry)\n\n\ncalculator.run()\n\n```\n\n```bash\ncalc:~$ add 2 2\n[+] 2 + 2 = 4\ncalc:~$ multiply 3 3\n[+] 3 * 3 = 9\ncalc:~$ memory\n2 + 2 = 4\n3 * 3 = 9\ncalc:~$ \n```\n\n## Manual\n### Command\nFirst and foremost you want to register some commands to make your REPL \nactionable. Adding command and bounding it with handling function is possible \nthrough `Riposte.command` decorator.\n\n```python\nfrom riposte import Riposte\n\nrepl = Riposte()\n\n@repl.command(\"hello\")\ndef hello():\n repl.success(\"Is it me you looking for?\")\n\nrepl.run()\n```\n```bash\nriposte:~ $ hello\n[+] Is it me you looking for?\n```\nAdditionally `Riposte.command` accepts few optional parameters:\n* `description` few words describing command which you can later use to build \nmeaningful help \n* [`guides`](#guides) definition of how to interpret passed arguments\n\n### Completer\n`Riposte` comes with support for tab-completion for commands. You can register \ncompleter function in a similar way you registering commands, just use `Riposte.complete` \ndecorator and point it to a specific command.\n\n```python\nfrom riposte import Riposte\n\nrepl = Riposte()\n\nSTART_SUBCOMMANDS = [\"foo\", \"bar\"]\n\n\n@repl.command(\"start\")\ndef start(subcommand: str):\n if subcommand in START_SUBCOMMANDS:\n repl.status(f\"{subcommand} started\")\n else:\n repl.error(\"Unknown subcommand.\")\n\n\n@repl.complete(\"start\")\ndef start_completer(text, line, start_index, end_index):\n\n return [\n subcommand\n for subcommand in START_SUBCOMMANDS\n if subcommand.startswith(text)\n ]\n\n\nrepl.run()\n\n```\nCompleter function is triggered by the TAB key. Every completer function should \nreturn list of valid options and should accept the following parameters:\n* `text` last word in the line\n* `line` content of the whole line\n* `start_index` starting index of the last word in the line\n* `end_index` ending index of the last word in the line\n\nSo in the case of our example:\n\n`riposte:~ $ start ba`\n\n```\ntext -> \"ba\"\nline -> \"start ba\"\nstart_index -> 6\nend_index -> 8\n```\nEquipped with this information you can build your custom completer functions for \nevery command.\n\n### Guides\nGuides is a way of saying how [command](#command) should interpret arguments \npassed by the user via prompt. `Riposte` rely on \n[type-hints](https://docs.python.org/3/library/typing.html) in order to do that.\n```python\nfrom riposte import Riposte\n\nrepl = Riposte()\n\n@repl.command(\"guideme\")\ndef guideme(x: int, y: str):\n repl.print(\"x:\", x, type(x))\n repl.print(\"y:\", y, type(y))\n\nrepl.run()\n```\n```bash\nriposte:~ $ guideme 1 1\nx: 1 \ny: 1 \n```\nIn both cases we've passed value _1_ as `x` and `y`. Based on \nparameter's type-hint passed arguments was interpreted as `int` in case of `x` \nand as `str` in case of `y`. You can also use this technique for different types.\n\n```python\nfrom riposte import Riposte\n\nrepl = Riposte()\n\n@repl.command(\"guideme\")\ndef guideme(x: dict, y: list):\n x[\"foo\"] = \"bar\"\n repl.print(\"x:\", x, type(x))\n\n y.append(\"foobar\")\n repl.print(\"y:\", y, type(y))\n\nrepl.run()\n```\n```bash\nriposte:~ $ guideme \"{'bar': 'baz'}\" \"['barbaz']\"\nx: {'bar': 'baz', 'foo': 'bar'} \ny: ['barbaz', 'foobar'] \n```\nAnother more powerful way of defining guides for handling function parameters \nis defining it straight from`Riposte.command` decorator. In this case guide\ndefined this way take precedence over the type hints.\n```python\nfrom riposte import Riposte\n\nrepl = Riposte()\n\n@repl.command(\"guideme\", guides={\"x\": [int]})\ndef guideme(x):\n repl.print(\"x:\", x, type(x))\n\nrepl.run()\n```\n```bash\nriposte:~ $ guideme 1\nx: 1 \n```\nWhy it is more powerful? Because this way you can chain different guides, \nwhere output of one guide is input for another, creating validation or cast \ninput into more complex types.\n```python\nfrom collections import namedtuple\n\nfrom riposte import Riposte\nfrom riposte.exceptions import RiposteException\nfrom riposte.guides import literal\n\nrepl = Riposte()\n\n\ndef non_negative(value: int):\n if value < 0:\n raise RiposteException(\"Value can't be negative\")\n\n return value\n\n\nPoint = namedtuple(\"Point\", (\"x\", \"y\"))\n\n\ndef get_point(value: dict):\n return Point(**value)\n\n\n@repl.command(\"guideme\",\n guides={\"x\": [int, non_negative], \"y\": [literal, get_point]})\ndef guideme(x, y):\n repl.print(\"x:\", x, type(x))\n repl.print(\"y:\", y, type(y))\n\n\nrepl.run()\n\n```\n```bash\nriposte:~ $ guideme -1 '{\"x\": 1, \"y\": 2}'\n[-] Value can't be negative\nriposte:~ $ guideme 1 '{\"x\": 1, \"y\": 2}'\nx: 1 \ny: Point(x=1, y=2) \nriposte:~ $ \n```\nUnder the hood, it is a simple function call where the input string is passed \nto first guide function in the chain. In this case, the call looks like this:\n```python\nnon_negative(int(\"-1\")) # guide chain for parameter `x`\nget_point(literal('{\"x\": 1, \"y\": 2}')) # guide chain for parameter `y`\n```\n\n### Printing\n_Riposte_ comes with built-in thread safe printing methods:\n\n* `print`\n* `info`\n* `error`\n* `status`\n* `success`\n\nEvery method follows the signature of Python's built-in \n[`print()`](https://docs.python.org/3/library/functions.html#print) function. \nBesides `print` all of them provide informative coloring corresponding to its name.\n\nWe strongly encourage to stick to our thread safe printing API but if you are \nfeeling frisky, know what you are doing and you are 100% sure, that threaded \nexecution is something that will never come up at some point in the lifecycle of \nyou application feel free to use Python's built-in \n[`print()`](https://docs.python.org/3/library/functions.html#print) function. \n\n#### Extending `PrinterMixin`\nIf you want to change the styling of existing methods or add custom one, please \nextend `PrinterMixin` class.\n\n```python\nfrom riposte import Riposte\nfrom riposte.printer.mixins import PrinterMixin\n\n\nclass ExtendedPrinterMixin(PrinterMixin):\n def success(self, *args, **kwargs): # overwriting existing method\n self.print(*args, **kwargs)\n\n def shout(self, *args, **kwargs): # adding new one\n self.print((*args, \"!!!\"), **kwargs)\n\nclass CustomRiposte(Riposte, ExtendedPrinterMixin):\n pass\n\nrepl = CustomRiposte()\n\n@repl.command(\"foobar\")\ndef foobar(message: str):\n repl.shout(message)\n\n```\n\n#### Customizing `PrinterMixin`\nNot happy about existing printing API? No problem, you can also build your own \nfrom scratch using `PrinterBaseMixin` and its thread safe `_print` method.\n\n```python\nfrom riposte import Riposte\nfrom riposte.printer.mixins import PrinterBaseMixin\n\n\nclass CustomPrinterMixin(PrinterBaseMixin):\n def ask(self, *args, **kwargs): # adding new one\n self._print((*args, \"???\"), **kwargs)\n\n def shout(self, *args, **kwargs): # adding new one\n self._print((*args, \"!!!\"), **kwargs)\n\nclass CustomRiposte(Riposte, CustomPrinterMixin):\n pass\n\nrepl = CustomRiposte()\n\n@repl.command(\"foobar\")\ndef foobar(message: str):\n repl.shout(message)\n repl.ask(message)\n repl.success(message) # It'll raise exception as it's no longer available\n```\n\n#### Coloring output with `Pallete`\nIf you feel like adding a few colors to the output you can always use `Pallete`.\n\n```python\nfrom riposte import Riposte\nfrom riposte.printer import Palette\n\n\nrepl = Riposte()\n\n\n@repl.command(\"foo\")\ndef foo(msg: str):\n repl.print(Palette.GREEN.format(msg)) # It will be green\n```\n\n`Pallete` goes with the following output formattings:\n* `GREY`\n* `RED`\n* `GREEN`\n* `YELLOW`\n* `BLUE`\n* `MAGENTA`\n* `CYAN`\n* `WHITE`\n* `BOLD`\n\n### History\nCommand history is stored in your HOME directory in `.riposte` file. \nThe default length is 100 lines. Both settings can be changed using \n`history_file` and `history_length` parameters.\n```python\nfrom pathlib import Path\nfrom riposte import Riposte\n\n\nrepl = Riposte(\n history_file=Path.home() / \".custom_history_file\", \n history_length=500,\n)\n```\n\n### Prompt\nThe default prompt is `riposte:~ $ ` but you can easily customize it:\n```python\nfrom riposte import Riposte\n\n\nrepl = Riposte(prompt=\"custom-prompt >>> \")\nrepl.run()\n```\n\nYou can also dynamically resolve prompt layout based on the state of some object \nsimply by overwriting `Riposte.prompt` property. In the following example, we'll \ndetermine prompt based on `MODULE` value:\n```python\nfrom riposte import Riposte\n\n\nclass Application:\n def __init__(self):\n self.module = None\n\n\nclass CustomRiposte(Riposte):\n @property\n def prompt(self):\n if app.module:\n return f\"foo:{app.module} > \"\n else:\n return self._prompt # reference to `prompt` parameter.\n\n\napp = Application()\nrepl = CustomRiposte(prompt=\"foo > \")\n\n\n@repl.command(\"set\")\ndef set_module(module_name: str):\n app.module = module_name\n repl.success(\"Module has been set.\")\n\n\n@repl.command(\"unset\")\ndef unset_module():\n app.module = None\n repl.success(\"Module has been unset.\")\n\n\nrepl.run()\n```\n\n```bash\nfoo > set bar\n[+] Module has been set.\nfoo:bar > unset\n[+] Module has been unset.\nfoo >\n```\n\n### Banner\n```python\n# banner.py\n\nfrom riposte import Riposte\n\nBANNER = \"\"\" _ _ _ _ _ _ _ _ _ \n| | | | | | | | | | | | | | | |\n| |_| | ___| | | ___ | | | | ___ _ __| | __| | |\n| _ |/ _ \\ | |/ _ \\ | |/\\| |/ _ \\| '__| |/ _` | |\n| | | | __/ | | (_) | \\ /\\ / (_) | | | | (_| |_|\n\\_| |_/\\___|_|_|\\___/ \\/ \\/ \\___/|_| |_|\\__,_(_)\nWelcome User Hello World v1.2.3\n\"\"\"\n\nrepl = Riposte(banner=BANNER)\n\n\n@repl.command(\"hello\")\ndef hello():\n repl.print(\"Hello World!\")\n\n\nrepl.run()\n```\n```bash\n$ python banner.py\n _ _ _ _ _ _ _ _ _ \n| | | | | | | | | | | | | | | |\n| |_| | ___| | | ___ | | | | ___ _ __| | __| | |\n| _ |/ _ \\ | |/ _ \\ | |/\\| |/ _ \\| '__| |/ _` | |\n| | | | __/ | | (_) | \\ /\\ / (_) | | | | (_| |_|\n\\_| |_/\\___|_|_|\\___/ \\/ \\/ \\___/|_| |_|\\__,_(_)\nWelcome User Hello World v1.2.3\n\nriposte:~ $\n```\nIf for some reason you don't want to display banner (Maybe you have custom \n[input stream](#adding-custom-input-stream)?) you can set `Riposte.print_banner` attribute \nto `False`.\n\n### Inline command execution\nSimilarly to the `bash` if you delimit commands with semicolon you can trigger \nexecution of multiple commands in one line.\n```bash\nriposte:~ $ hello; hello; hello\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n```\n`Riposte` also exposes CLI for your applications which gives you the ability to \npass commands using `-c` switch:\n```bash\n$ python app.py -c \"hello; hello; hello;\"\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n$ \n```\nGiven all of this, you can also start to treat your application as something \nthat could be turned into automated scripts.\n\n### CLI\nIf you application needs custom CLI arguments _Riposte_ gives you way to \nimplement it by overwriting `Riposte.setup_cli()` method. Let's say you want to \nintroduce `--verbose` flag into your application:\n```python\n# custom_cli_args.py\n\nfrom riposte import Riposte\n\n\nclass CustomArgsRiposte(Riposte):\n def setup_cli(self):\n super().setup_cli() # preserve default Riposte CLI\n\n self.parser.add_argument(\n \"-v\", \"--verbose\", action=\"store_true\", help=\"Verbose mode\"\n )\n\n\nrepl = CustomArgsRiposte()\n\n\n@repl.command(\"foo\")\ndef foo(bar: str):\n repl.success(\"Foobar executed.\")\n\n if repl.arguments.verbose:\n repl.success(\"Argument passed as bar: \", bar)\n\n\nrepl.run()\n\n```\n```bash\n$ python custom_cli_args.py -v\nriposte:~ $ foo 123\n[+] Foobar executed.\n[+] Argument passed as bar: 123\nriposte:~ $ \n```\n`Riposte.parser` is an instance of Python's builtin [`argparse.ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser) \nso for all further instructions regarding adding CLI arguments please follow \n[`argparse`](https://docs.python.org/3/library/argparse.html#module-argparse) \ndocumentation.\n\nPassed arguments are being parsed in `Riposte.run()` and stored in \n`Riposte.arguments` so you can access it within your application. If you need \nto access them before entering the main evaluation loop you can overwrite \n`Riposte.parse_cli_arguments()`\n```python\nfrom riposte import Riposte\n\n\nclass CustomArgsRiposte(Riposte):\n def setup_cli(self):\n super().setup_cli() # preserve default Riposte CLI\n\n self.parser.add_argument(\n \"-v\", \"--verbose\", action=\"store_true\", help=\"Verbose mode\"\n )\n\n def parse_cli_arguments(self):\n super().parse_cli_arguments() # preserve default Riposte CLI\n\n if self.arguments.verbose:\n do_something_specific()\n```\n\n### Input streams\nThe input stream is an abstraction telling how you feed _Riposte_ with \ncommands. Right now you can use following ones out of the box.\n#### Prompt\nDefault one which allows you input commands using the traditional prompt.\n#### CLI\n`Riposte` also exposes CLI for your applications which gives you the ability \nto pass commands using `-c` switch:\n```bash\n$ python app.py -c \"hello; hello; hello;\"\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n```\n#### File\nYou can also pass text file containing commands as an argument to your \napplication:\n```python\n# demo.py\n\nfrom riposte import Riposte\n\nrepl = Riposte()\n\n@repl.command(\"hello\")\ndef hello():\n repl.print(\"Is it me you looking for?\")\n\nrepl.run()\n```\n`commands.rpst` text file containing commands to be executed:\n```\nhello\nhello\nhello\n```\n```bash\n$ python demo.py commands.rpst\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n[+] Is it me you looking for?\n```\n#### Adding custom input stream\nIf for some reason you need a custom way of feeding _Riposte_ with commands \nyou can always add your custom input stream. The input stream is a generator \nthat yields function which after calling it returns a string (the command) \n`Generator[Callable[[], str], None, None]`. Let's say you are an evil genius \nand want to make your interactive shell application less interactive by \nfeeding it with some kind of messaging system.\n```python\nimport itertools\nfrom typing import Callable, Generator\n\nfrom riposte import Riposte\nfrom some.messaging.system import Subscriber\n\n\ndef some_messaging_system_input_stream(\n subscriber: Subscriber # you can parametrize your input streams\n) -> Generator[Callable, None, None]:\n # itertools.repeat() make sure that your input stream runs forever\n yield from itertools.repeat(subscriber.poll) # calling poll() will return command\n\n\nclass CustomInputStreamRiposte(Riposte):\n def setup_cli(self):\n super().setup_cli() # preserve default Riposte CLI\n\n self.parser.add_argument(\n \"-h\", \"--host\", help=\"Some messaging system address\"\n )\n\n def parse_cli_arguments(self) -> None:\n super().parse_cli_arguments() # preserve default Riposte CLI\n\n if self.arguments.host:\n subscriber = Subscriber(self.arguments.host)\n self.input_stream = some_messaging_system_input_stream(subscriber)\n self.print_banner = False # I guess you don't want to print banner \n```\n\n## Project status\n_Riposte_ is under development. It might be considered to be in beta phase. \nThere might be some breaking changes in the future although a lot of concepts \npresent here was already battle-tested during \n[routersploit](https://github.com/threat9/routersploit) development.\n\n## Contributing\nPlease read [CONTRIBUTING.md]() for details on our code of conduct, and the \nprocess for submitting pull requests to us.\n\n## Versioning\nProject uses [SemVer](http://semver.org/) versioning. For the versions \navailable, see the [releases](https://github.com/fwkz/riposte/releases). \n\n## License\n_Riposte_ is licensed under the MIT License - see the \n[LICENSE](https://github.com/fwkz/riposte/blob/master/LICENSE) file for details\n\n## Acknowledgments\n* [routersploit](https://github.com/threat9/routersploit)\n* [click](https://click.palletsprojects.com/)\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/fwkz/riposte", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "riposte", "package_url": "https://pypi.org/project/riposte/", "platform": "", "project_url": "https://pypi.org/project/riposte/", "project_urls": { "Homepage": "https://github.com/fwkz/riposte" }, "release_url": "https://pypi.org/project/riposte/0.4.0/", "requires_dist": [ "black ; extra == 'dev'", "flake8 ; extra == 'dev'", "isort ; extra == 'dev'", "pytest ; extra == 'dev'", "twine ; extra == 'dev'" ], "requires_python": ">=3.6", "summary": "Package for wrapping applications inside a tailored interactive shell.", "version": "0.4.0" }, "last_serial": 5694170, "releases": { "0.2.1": [ { "comment_text": "", "digests": { "md5": "62bec961588ca4144f8438cafe61b658", "sha256": "fbb4fbdeef58d089a051e9fd3386789e2c34b23b54b325eed9ca10a2bb9e1967" }, "downloads": -1, "filename": "riposte-0.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "62bec961588ca4144f8438cafe61b658", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 7546, "upload_time": "2019-02-23T17:36:25", "url": "https://files.pythonhosted.org/packages/f3/9c/d3c95fa4be9261c01be3fc34f3706bced27432eaee6e3b160ef6f600cbca/riposte-0.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "e4f999d5ee93b34b483669942160037f", "sha256": "44f8578467c627b23a74bcef33f505c8f4eaf0b22fcd2e3f7b152170e3c3ba29" }, "downloads": -1, "filename": "riposte-0.2.1.tar.gz", "has_sig": false, "md5_digest": "e4f999d5ee93b34b483669942160037f", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 8521, "upload_time": "2019-02-23T17:36:27", "url": "https://files.pythonhosted.org/packages/07/39/735244c7d80bf4780af097267e2ee6ab159ade1a060956b6b0116109832a/riposte-0.2.1.tar.gz" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "ecbaf93318cb4517d527bae06896449d", "sha256": "230c1eb2f14929f4d41ac9dfa6053a5002c2ca68a4b4650542db9ef1b96f31a5" }, "downloads": -1, "filename": "riposte-0.2.2-py3-none-any.whl", "has_sig": false, "md5_digest": "ecbaf93318cb4517d527bae06896449d", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 12943, "upload_time": "2019-06-30T18:01:49", "url": "https://files.pythonhosted.org/packages/25/e1/7d8103772d511f6a1422c96ef72941cb0f951883dd42c404af6a531f5a7a/riposte-0.2.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "346a868fdbb45079f6d48f55d8799d54", "sha256": "2331a1e949ba29807975d552db56517201f2bdb6b3209b532d6c148b9eb859cb" }, "downloads": -1, "filename": "riposte-0.2.2.tar.gz", "has_sig": false, "md5_digest": "346a868fdbb45079f6d48f55d8799d54", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 19956, "upload_time": "2019-06-30T18:01:51", "url": "https://files.pythonhosted.org/packages/fd/9f/5a894465c641bada67ce51e74b4807793c903d31ed1ff5b35e2c386ffd60/riposte-0.2.2.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "90be951d51cd140215639abf6c6d260c", "sha256": "85f26e47b52d7927da87e8ac3ae207685fa1ca54c915aa050e3fbf273c32cf63" }, "downloads": -1, "filename": "riposte-0.3.0-py3-none-any.whl", "has_sig": false, "md5_digest": "90be951d51cd140215639abf6c6d260c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 14250, "upload_time": "2019-07-27T20:55:25", "url": "https://files.pythonhosted.org/packages/75/42/a246ddaa8ad29354157ddb328ebc4fd7d27d6ca0175de2f6fc16d3efde26/riposte-0.3.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "5b41561e505bf1d3428ca13202452ac4", "sha256": "de6b463d7c99e2936cbbf7873e139cfa894d27753b2eaf100cf04f3f19c99de5" }, "downloads": -1, "filename": "riposte-0.3.0.tar.gz", "has_sig": false, "md5_digest": "5b41561e505bf1d3428ca13202452ac4", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 22145, "upload_time": "2019-07-27T20:55:27", "url": "https://files.pythonhosted.org/packages/9e/22/2ac514e6d9fd8ac8ec5c9573266017059691480e536c91e18469bee9f214/riposte-0.3.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "938730e73c506c5e3aebb6683a4762cb", "sha256": "db627a76deedec13d3cdbca5def0f38dfe6304d6c55ce25ce20f3adfdfc234cd" }, "downloads": -1, "filename": "riposte-0.4.0-py3-none-any.whl", "has_sig": false, "md5_digest": "938730e73c506c5e3aebb6683a4762cb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 15760, "upload_time": "2019-08-18T10:56:28", "url": "https://files.pythonhosted.org/packages/f3/1c/bfabb671353f9d026b5137fb3841481e264be7731f6373592b0f4409d184/riposte-0.4.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "2acca00f72b6b26bd92c4954576d4add", "sha256": "adefb2b4815efa10ffef9757ffd769a7aec9beb43cfb7b96d7b28f1419affd68" }, "downloads": -1, "filename": "riposte-0.4.0.tar.gz", "has_sig": false, "md5_digest": "2acca00f72b6b26bd92c4954576d4add", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 25511, "upload_time": "2019-08-18T10:56:30", "url": "https://files.pythonhosted.org/packages/da/d0/8fc1c5808a6fc5859ebf205da91fd47c0c494616e938c48a98988129e46b/riposte-0.4.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "938730e73c506c5e3aebb6683a4762cb", "sha256": "db627a76deedec13d3cdbca5def0f38dfe6304d6c55ce25ce20f3adfdfc234cd" }, "downloads": -1, "filename": "riposte-0.4.0-py3-none-any.whl", "has_sig": false, "md5_digest": "938730e73c506c5e3aebb6683a4762cb", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6", "size": 15760, "upload_time": "2019-08-18T10:56:28", "url": "https://files.pythonhosted.org/packages/f3/1c/bfabb671353f9d026b5137fb3841481e264be7731f6373592b0f4409d184/riposte-0.4.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "2acca00f72b6b26bd92c4954576d4add", "sha256": "adefb2b4815efa10ffef9757ffd769a7aec9beb43cfb7b96d7b28f1419affd68" }, "downloads": -1, "filename": "riposte-0.4.0.tar.gz", "has_sig": false, "md5_digest": "2acca00f72b6b26bd92c4954576d4add", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6", "size": 25511, "upload_time": "2019-08-18T10:56:30", "url": "https://files.pythonhosted.org/packages/da/d0/8fc1c5808a6fc5859ebf205da91fd47c0c494616e938c48a98988129e46b/riposte-0.4.0.tar.gz" } ] }