piou


Namepiou JSON
Version 0.14.2 PyPI version JSON
download
home_pagehttps://github.com/andarius/piou
SummaryA CLI toolkit
upload_time2023-12-15 10:30:13
maintainer
docs_urlNone
authorJulien Brayere
requires_python>=3.9,<4.0
licenseMIT
keywords cli
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/Andarius/piou/raw/dev/docs/piou-dark.png">
  <source media="(prefers-color-scheme: light)" srcset="https://github.com/Andarius/piou/raw/dev/docs/piou.jpg">
  <img alt="Piou logo" 
    src="https://github.com/Andarius/piou/raw/dev/docs/piou.jpg"
    width="250"/>
</picture>

# Piou

[![Python versions](https://img.shields.io/pypi/pyversions/piou)](https://pypi.python.org/pypi/piou)
[![Latest PyPI version](https://img.shields.io/pypi/v/piou?logo=pypi)](https://pypi.python.org/pypi/piou)
[![CircleCI](https://circleci.com/gh/Andarius/piou/tree/master.svg?style=shield)](https://app.circleci.com/pipelines/github/Andarius/piou?branch=master)
[![Latest conda-forge version](https://img.shields.io/conda/vn/conda-forge/piou?logo=conda-forge)](https://anaconda.org/conda-forge/piou)

A CLI tool to build beautiful command-line interfaces with type validation.

It is as simple as

```python
from piou import Cli, Option

cli = Cli(description='A CLI tool')


@cli.command(cmd='foo', help='Run foo command')
def foo_main(
        bar: int = Option(..., help='Bar positional argument (required)'),
        baz: str = Option(..., '-b', '--baz', help='Baz keyword argument (required)'),
        foo: str = Option(None, '--foo', help='Foo keyword argument'),
):
    """
    A longer description on what the function is doing.
    You can run it with:
    ```bash
     poetry run python -m piou.test.simple foo 1 -b baz
    ```
    And you are good to go!
    """
    pass


if __name__ == '__main__':
    cli.run()
```

The output will look like this:

- `python -m piou.test.simple -h`

![example](https://github.com/Andarius/piou/raw/master/docs/simple-output.png)

- `python -m piou.test.simple foo -h`

![example](https://github.com/Andarius/piou/raw/master/docs/simple-output-foo.png)

# Why ?

I could not find a library that provided:

- the same developer experience than [FastAPI](https://fastapi.tiangolo.com/)
- customization of the interface (to build a CLI similar to the one of [Poetry](https://python-poetry.org/))
- type validation / casting

[Typer](https://github.com/tiangolo/typer) is the closest alternative in terms of experience but lacks the possibility
to format the output is a custom way using external libraries (like [Rich](https://github.com/Textualize/rich)).

**Piou** provides all these possibilities and lets you define your own [Formatter](#custom-formatter).

# Install

You can install `piou` with either:

- `pip install piou`
- `conda install piou -c conda-forge`

# Features

## Commands

```python
from piou import Cli, Option

cli = Cli(description='A CLI tool')


@cli.command(cmd='foo',
             help='Run foo command')
def foo_main(
        foo1: int = Option(..., help='Foo arguments'),
        foo2: str = Option(..., '-f', '--foo2', help='Foo2 arguments'),
        foo3: str = Option(None, '-g', '--foo3', help='Foo3 arguments'),
):
    pass


@cli.command(cmd='bar',
             help='Run bar command')
def bar_main(
        foo1: int = Option(..., help='Foo arguments'),
        foo2: str = Option(..., '-f', '--foo2', help='Foo2 arguments'),
        foo3: str = Option(None, '-g', '--foo3', help='Foo3 arguments'),
):
    pass


if __name__ == '__main__':
    cli.run()
```

In this case, `foo1` is a positional argument while `foo2` and `foo3` are keyword arguments.

You can optionally specify global options that will be passed to all commands:

```python
cli = Cli(description='A CLI tool')

cli.add_option('-q', '--quiet', help='Do not output any message')
```

The **description** can also be extracted from the function docstring. Both functions here return the same description.

```python
@cli.command(cmd='bar', description='Run foo command')
def bar_main():
    pass


@cli.command(cmd='bar2')
def bar_2_main():
    """
    Run foo command
    """
    pass
```

A command can also be asynchronous, it will be run automatically using `asyncio.run`.

```python
@cli.command(cmd='bar', help='Run foo command')
async def bar_main():
    pass
```

## Command Groups / Sub-commands

You can group commands into sub-commands:

```python
from piou import Cli, Option

cli = Cli(description='A CLI tool')


@cli.command(cmd='foo', help='Run foo command')
def foo_main():
    pass


sub_cmd = cli.add_sub_parser(cmd='sub', help='A sub command')
sub_cmd.add_option('--test', help='Test mode')


@sub_cmd.command(cmd='bar', help='Run bar command')
def sub_bar_main(**kwargs):
    pass


@sub_cmd.command(cmd='foo', help='Run foo command')
def sub_foo_main(
        test: bool,
        foo1: int = Option(..., help='Foo argument'),
        foo2: str = Option(..., '-f', '--foo2', help='Foo2 argument'),
):
    pass


if __name__ == '__main__':
    cli.run()
```

So when running `python run.py sub -h` it will output the following:

![example](https://github.com/Andarius/piou/raw/master/docs/sub-cmd-output.png)

## Options processor

Sometimes, you want to run a function using the global arguments before running the actual command (for instance
initialize a logger based on the `verbose` level).

To do so, you use `set_options_processor` that will receive all the current global options of the CLI.

```python
from piou import Cli

cli = Cli(description='A CLI tool')

cli.add_option('--verbose', help='Increase verbosity')


def processor(verbose: bool):
    print(f'Processing {verbose=}')


cli.set_options_processor(processor)
```

You can also use the decorator syntax:

```python
from piou import Cli, Option

cli = Cli(description='A CLI tool')


@cli.processor()
def processor(verbose: bool = Option(False, '--verbose', help='Increase verbosity')):
    print(f'Processing {verbose=}')
```

By default, when a processor is set, the global arguments will not be passed downstream.
If you still want them to be passed to the functions by setting

```python
cli = Cli(description='A CLI tool', propagate_options=True)
```

or in the case of a **sub-command**

```python
cli.add_sub_parser(cmd='sub', help='A sub command', propagate_options=True)
```

## Derived Options

Sometimes, you want to reuse the options in multiple command and group them into a single output to pass to
the command. For instance, you might want to group a connection string parameter to connect to a database. Here is a
full example:

```python
from piou import Cli, Option, Derived, Password
import psycopg2

cli = Cli(description='A CLI tool')


def get_pg_conn(
        pg_user: str = Option('postgres', '--pg-user'),
        pg_pwd: Password = Option('postgres', '--pg-pwd'),
        pg_host: str = Option('localhost', '--pg-host'),
        pg_port: int = Option(5432, '--pg-port'),
        pg_db: str = Option('postgres', '--pg-db')

):
    conn = psycopg2.connect(dbname=pg_db, user=pg_user, password=pg_pwd,
                            host=pg_host, port=pg_port)
    return conn


@cli.command(help='Run foo command')
def foo(pg_conn=Derived(get_pg_conn)):
    ...


@cli.command(help='Run bar command')
def bar(pg_conn=Derived(get_pg_conn)):
    ...
```

You can also pass dynamic derived functions to avoid duplicating the derived logic:

```python
import os
from typing import Literal
from piou import Cli, Option, Derived

cli = Cli(description='A CLI tool')


def get_pg_url_dynamic(source: Literal['db1', 'db2']):
    _source_upper = source.upper()
    _host_arg = f'--host-{source}'
    _db_arg = f'--{source}'

    def _derived(
            # We need to specify the `arg_name` here
            pg_host: str = Option(os.getenv(f'PG_HOST_{_source_upper}', 'localhost'),
                                  _host_arg, arg_name=_host_arg),
            pg_db: str = Option(os.getenv(f'PG_DB_{_source_upper}', source),
                                _db_arg, arg_name=_db_arg),
    ):
        return f'postgresql://postgres:postgres@{pg_host}:5432/{pg_db}'

    return _derived


@cli.command(help='Run dynamic command')
def dynamic(url_1: str = Derived(get_pg_url_dynamic('db1')),
            url_2: str = Derived(get_pg_url_dynamic('db2'))):
    ...
```

So that the output will look like this:

![dynamic-derived](https://github.com/Andarius/piou/raw/master/docs/dynamic-derived.png)

## On Command Run

If you want to get the command name and arguments information that are passed to it (in case of general purpose
debugging for instance), you can pass `on_cmd_run` to the CLI.

```python
from piou import Cli, Option, CommandMeta, Derived


def on_cmd_run(meta: CommandMeta):
    pass


cli = Cli(description='A CLI tool',
          on_cmd_run=on_cmd_run)


def processor(a: int = Option(1, '-a'),
              b: int = Option(2, '-b')):
    return a + b


@cli.command()
def test(
        value: int = Derived(processor),
        bar: str = Option(None, '--bar')
):
    pass
```

In this case, `meta` will be equal to:

```python
CommandMeta(cmd_name='test',
            fn_args={'bar': 'bar', 'value': 5},
            cmd_args={'a': 3, 'b': 2, 'bar': 'bar'})
```

## Help / Errors Formatter

You can customize the help and the different errors displayed by the CLI by passing a Formatter.
The default one is the **Rich formatter** based on the [Rich](https://github.com/Textualize/rich) package:

- `cmd_color`: set the color of the command in the help
- `option_color`: set the color of the positional / keyword arguments in the help
- `default_color`: set the color of the default values in the help
- `show_default`: show the default values if the keyword arguments (if available)

You can create your own Formatter by subclassing the `Formatter` class (see
the [Rich formatter](https://github.com/Andarius/piou/blob/master/piou/formatter/rich_formatter.py)
for example).

The **Rich Formatter** supports the `Password` type that will hide the default value when printing help.  
For instance:

```python
from piou import Password, Option


def test(pg_pwd: Password = Option('postgres', '--pg-pwd')):
    ...
```

## Complete example

You can try a more complete example by running `python -m piou.test -h`

## Moving from `argparse`

If you are migrating code from `argparse` to `piou` here are some differences:

### 1. choices:

`add_argument('--pick', choices=['foo', 'bar'])`  
can be replaced with the following:

- `pick: Literal['foo', 'bar'] = Option(None, '--pick')`
- `pick: Literal['foo'] | Literal['bar'] = Option(None, '--pick')`
- `pick: str = Option(None, '--pick', choices=['foo', 'bar'])`

**Notes**:

- You can disable the case sensitivity by passing `Option(None, '--pick', case_sentitive=False)`
- Specifying both a `Literal` type and `choices` will raise an error.

### 2. action=store_true:

`add_argument('--verbose', action='store_true')`  
can be replaced with  
`verbose: bool = Option(False, '--verbose')`

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/andarius/piou",
    "name": "piou",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.9,<4.0",
    "maintainer_email": "",
    "keywords": "cli",
    "author": "Julien Brayere",
    "author_email": "julien.brayere@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/23/15/52d5ff97e84248cb3782721047e4b1376cec34ba29ea4fcf66d9748af440/piou-0.14.2.tar.gz",
    "platform": null,
    "description": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/Andarius/piou/raw/dev/docs/piou-dark.png\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/Andarius/piou/raw/dev/docs/piou.jpg\">\n  <img alt=\"Piou logo\" \n    src=\"https://github.com/Andarius/piou/raw/dev/docs/piou.jpg\"\n    width=\"250\"/>\n</picture>\n\n# Piou\n\n[![Python versions](https://img.shields.io/pypi/pyversions/piou)](https://pypi.python.org/pypi/piou)\n[![Latest PyPI version](https://img.shields.io/pypi/v/piou?logo=pypi)](https://pypi.python.org/pypi/piou)\n[![CircleCI](https://circleci.com/gh/Andarius/piou/tree/master.svg?style=shield)](https://app.circleci.com/pipelines/github/Andarius/piou?branch=master)\n[![Latest conda-forge version](https://img.shields.io/conda/vn/conda-forge/piou?logo=conda-forge)](https://anaconda.org/conda-forge/piou)\n\nA CLI tool to build beautiful command-line interfaces with type validation.\n\nIt is as simple as\n\n```python\nfrom piou import Cli, Option\n\ncli = Cli(description='A CLI tool')\n\n\n@cli.command(cmd='foo', help='Run foo command')\ndef foo_main(\n        bar: int = Option(..., help='Bar positional argument (required)'),\n        baz: str = Option(..., '-b', '--baz', help='Baz keyword argument (required)'),\n        foo: str = Option(None, '--foo', help='Foo keyword argument'),\n):\n    \"\"\"\n    A longer description on what the function is doing.\n    You can run it with:\n    ```bash\n     poetry run python -m piou.test.simple foo 1 -b baz\n    ```\n    And you are good to go!\n    \"\"\"\n    pass\n\n\nif __name__ == '__main__':\n    cli.run()\n```\n\nThe output will look like this:\n\n- `python -m piou.test.simple -h`\n\n![example](https://github.com/Andarius/piou/raw/master/docs/simple-output.png)\n\n- `python -m piou.test.simple foo -h`\n\n![example](https://github.com/Andarius/piou/raw/master/docs/simple-output-foo.png)\n\n# Why ?\n\nI could not find a library that provided:\n\n- the same developer experience than [FastAPI](https://fastapi.tiangolo.com/)\n- customization of the interface (to build a CLI similar to the one of [Poetry](https://python-poetry.org/))\n- type validation / casting\n\n[Typer](https://github.com/tiangolo/typer) is the closest alternative in terms of experience but lacks the possibility\nto format the output is a custom way using external libraries (like [Rich](https://github.com/Textualize/rich)).\n\n**Piou** provides all these possibilities and lets you define your own [Formatter](#custom-formatter).\n\n# Install\n\nYou can install `piou` with either:\n\n- `pip install piou`\n- `conda install piou -c conda-forge`\n\n# Features\n\n## Commands\n\n```python\nfrom piou import Cli, Option\n\ncli = Cli(description='A CLI tool')\n\n\n@cli.command(cmd='foo',\n             help='Run foo command')\ndef foo_main(\n        foo1: int = Option(..., help='Foo arguments'),\n        foo2: str = Option(..., '-f', '--foo2', help='Foo2 arguments'),\n        foo3: str = Option(None, '-g', '--foo3', help='Foo3 arguments'),\n):\n    pass\n\n\n@cli.command(cmd='bar',\n             help='Run bar command')\ndef bar_main(\n        foo1: int = Option(..., help='Foo arguments'),\n        foo2: str = Option(..., '-f', '--foo2', help='Foo2 arguments'),\n        foo3: str = Option(None, '-g', '--foo3', help='Foo3 arguments'),\n):\n    pass\n\n\nif __name__ == '__main__':\n    cli.run()\n```\n\nIn this case, `foo1` is a positional argument while `foo2` and `foo3` are keyword arguments.\n\nYou can optionally specify global options that will be passed to all commands:\n\n```python\ncli = Cli(description='A CLI tool')\n\ncli.add_option('-q', '--quiet', help='Do not output any message')\n```\n\nThe **description** can also be extracted from the function docstring. Both functions here return the same description.\n\n```python\n@cli.command(cmd='bar', description='Run foo command')\ndef bar_main():\n    pass\n\n\n@cli.command(cmd='bar2')\ndef bar_2_main():\n    \"\"\"\n    Run foo command\n    \"\"\"\n    pass\n```\n\nA command can also be asynchronous, it will be run automatically using `asyncio.run`.\n\n```python\n@cli.command(cmd='bar', help='Run foo command')\nasync def bar_main():\n    pass\n```\n\n## Command Groups / Sub-commands\n\nYou can group commands into sub-commands:\n\n```python\nfrom piou import Cli, Option\n\ncli = Cli(description='A CLI tool')\n\n\n@cli.command(cmd='foo', help='Run foo command')\ndef foo_main():\n    pass\n\n\nsub_cmd = cli.add_sub_parser(cmd='sub', help='A sub command')\nsub_cmd.add_option('--test', help='Test mode')\n\n\n@sub_cmd.command(cmd='bar', help='Run bar command')\ndef sub_bar_main(**kwargs):\n    pass\n\n\n@sub_cmd.command(cmd='foo', help='Run foo command')\ndef sub_foo_main(\n        test: bool,\n        foo1: int = Option(..., help='Foo argument'),\n        foo2: str = Option(..., '-f', '--foo2', help='Foo2 argument'),\n):\n    pass\n\n\nif __name__ == '__main__':\n    cli.run()\n```\n\nSo when running `python run.py sub -h` it will output the following:\n\n![example](https://github.com/Andarius/piou/raw/master/docs/sub-cmd-output.png)\n\n## Options processor\n\nSometimes, you want to run a function using the global arguments before running the actual command (for instance\ninitialize a logger based on the `verbose` level).\n\nTo do so, you use `set_options_processor` that will receive all the current global options of the CLI.\n\n```python\nfrom piou import Cli\n\ncli = Cli(description='A CLI tool')\n\ncli.add_option('--verbose', help='Increase verbosity')\n\n\ndef processor(verbose: bool):\n    print(f'Processing {verbose=}')\n\n\ncli.set_options_processor(processor)\n```\n\nYou can also use the decorator syntax:\n\n```python\nfrom piou import Cli, Option\n\ncli = Cli(description='A CLI tool')\n\n\n@cli.processor()\ndef processor(verbose: bool = Option(False, '--verbose', help='Increase verbosity')):\n    print(f'Processing {verbose=}')\n```\n\nBy default, when a processor is set, the global arguments will not be passed downstream.\nIf you still want them to be passed to the functions by setting\n\n```python\ncli = Cli(description='A CLI tool', propagate_options=True)\n```\n\nor in the case of a **sub-command**\n\n```python\ncli.add_sub_parser(cmd='sub', help='A sub command', propagate_options=True)\n```\n\n## Derived Options\n\nSometimes, you want to reuse the options in multiple command and group them into a single output to pass to\nthe command. For instance, you might want to group a connection string parameter to connect to a database. Here is a\nfull example:\n\n```python\nfrom piou import Cli, Option, Derived, Password\nimport psycopg2\n\ncli = Cli(description='A CLI tool')\n\n\ndef get_pg_conn(\n        pg_user: str = Option('postgres', '--pg-user'),\n        pg_pwd: Password = Option('postgres', '--pg-pwd'),\n        pg_host: str = Option('localhost', '--pg-host'),\n        pg_port: int = Option(5432, '--pg-port'),\n        pg_db: str = Option('postgres', '--pg-db')\n\n):\n    conn = psycopg2.connect(dbname=pg_db, user=pg_user, password=pg_pwd,\n                            host=pg_host, port=pg_port)\n    return conn\n\n\n@cli.command(help='Run foo command')\ndef foo(pg_conn=Derived(get_pg_conn)):\n    ...\n\n\n@cli.command(help='Run bar command')\ndef bar(pg_conn=Derived(get_pg_conn)):\n    ...\n```\n\nYou can also pass dynamic derived functions to avoid duplicating the derived logic:\n\n```python\nimport os\nfrom typing import Literal\nfrom piou import Cli, Option, Derived\n\ncli = Cli(description='A CLI tool')\n\n\ndef get_pg_url_dynamic(source: Literal['db1', 'db2']):\n    _source_upper = source.upper()\n    _host_arg = f'--host-{source}'\n    _db_arg = f'--{source}'\n\n    def _derived(\n            # We need to specify the `arg_name` here\n            pg_host: str = Option(os.getenv(f'PG_HOST_{_source_upper}', 'localhost'),\n                                  _host_arg, arg_name=_host_arg),\n            pg_db: str = Option(os.getenv(f'PG_DB_{_source_upper}', source),\n                                _db_arg, arg_name=_db_arg),\n    ):\n        return f'postgresql://postgres:postgres@{pg_host}:5432/{pg_db}'\n\n    return _derived\n\n\n@cli.command(help='Run dynamic command')\ndef dynamic(url_1: str = Derived(get_pg_url_dynamic('db1')),\n            url_2: str = Derived(get_pg_url_dynamic('db2'))):\n    ...\n```\n\nSo that the output will look like this:\n\n![dynamic-derived](https://github.com/Andarius/piou/raw/master/docs/dynamic-derived.png)\n\n## On Command Run\n\nIf you want to get the command name and arguments information that are passed to it (in case of general purpose\ndebugging for instance), you can pass `on_cmd_run` to the CLI.\n\n```python\nfrom piou import Cli, Option, CommandMeta, Derived\n\n\ndef on_cmd_run(meta: CommandMeta):\n    pass\n\n\ncli = Cli(description='A CLI tool',\n          on_cmd_run=on_cmd_run)\n\n\ndef processor(a: int = Option(1, '-a'),\n              b: int = Option(2, '-b')):\n    return a + b\n\n\n@cli.command()\ndef test(\n        value: int = Derived(processor),\n        bar: str = Option(None, '--bar')\n):\n    pass\n```\n\nIn this case, `meta` will be equal to:\n\n```python\nCommandMeta(cmd_name='test',\n            fn_args={'bar': 'bar', 'value': 5},\n            cmd_args={'a': 3, 'b': 2, 'bar': 'bar'})\n```\n\n## Help / Errors Formatter\n\nYou can customize the help and the different errors displayed by the CLI by passing a Formatter.\nThe default one is the **Rich formatter** based on the [Rich](https://github.com/Textualize/rich) package:\n\n- `cmd_color`: set the color of the command in the help\n- `option_color`: set the color of the positional / keyword arguments in the help\n- `default_color`: set the color of the default values in the help\n- `show_default`: show the default values if the keyword arguments (if available)\n\nYou can create your own Formatter by subclassing the `Formatter` class (see\nthe [Rich formatter](https://github.com/Andarius/piou/blob/master/piou/formatter/rich_formatter.py)\nfor example).\n\nThe **Rich Formatter** supports the `Password` type that will hide the default value when printing help.  \nFor instance:\n\n```python\nfrom piou import Password, Option\n\n\ndef test(pg_pwd: Password = Option('postgres', '--pg-pwd')):\n    ...\n```\n\n## Complete example\n\nYou can try a more complete example by running `python -m piou.test -h`\n\n## Moving from `argparse`\n\nIf you are migrating code from `argparse` to `piou` here are some differences:\n\n### 1. choices:\n\n`add_argument('--pick', choices=['foo', 'bar'])`  \ncan be replaced with the following:\n\n- `pick: Literal['foo', 'bar'] = Option(None, '--pick')`\n- `pick: Literal['foo'] | Literal['bar'] = Option(None, '--pick')`\n- `pick: str = Option(None, '--pick', choices=['foo', 'bar'])`\n\n**Notes**:\n\n- You can disable the case sensitivity by passing `Option(None, '--pick', case_sentitive=False)`\n- Specifying both a `Literal` type and `choices` will raise an error.\n\n### 2. action=store_true:\n\n`add_argument('--verbose', action='store_true')`  \ncan be replaced with  \n`verbose: bool = Option(False, '--verbose')`\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A CLI toolkit",
    "version": "0.14.2",
    "project_urls": {
        "Homepage": "https://github.com/andarius/piou",
        "Repository": "https://github.com/andarius/piou"
    },
    "split_keywords": [
        "cli"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bde1b05afa8838c5eef1424648708a3d58e255f4a9f3fd2d8c69a2e4cbca89f2",
                "md5": "b8b185a96f7076585340323610b38a08",
                "sha256": "9db6c6b55dbb920d9f66bca9fa45545fae88b8ffab073df128af6abbd3e8dbbe"
            },
            "downloads": -1,
            "filename": "piou-0.14.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b8b185a96f7076585340323610b38a08",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9,<4.0",
            "size": 22289,
            "upload_time": "2023-12-15T10:30:11",
            "upload_time_iso_8601": "2023-12-15T10:30:11.327632Z",
            "url": "https://files.pythonhosted.org/packages/bd/e1/b05afa8838c5eef1424648708a3d58e255f4a9f3fd2d8c69a2e4cbca89f2/piou-0.14.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "231552d5ff97e84248cb3782721047e4b1376cec34ba29ea4fcf66d9748af440",
                "md5": "560cbde754e17be21016343974a38f86",
                "sha256": "650ccb36c40bbfeff32b324040c2e9ecafef7c16530942cf6ffdf909c84d3e41"
            },
            "downloads": -1,
            "filename": "piou-0.14.2.tar.gz",
            "has_sig": false,
            "md5_digest": "560cbde754e17be21016343974a38f86",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9,<4.0",
            "size": 22201,
            "upload_time": "2023-12-15T10:30:13",
            "upload_time_iso_8601": "2023-12-15T10:30:13.395613Z",
            "url": "https://files.pythonhosted.org/packages/23/15/52d5ff97e84248cb3782721047e4b1376cec34ba29ea4fcf66d9748af440/piou-0.14.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-12-15 10:30:13",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "andarius",
    "github_project": "piou",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "circle": true,
    "lcname": "piou"
}
        
Elapsed time: 0.50482s