Name | piou JSON |
Version |
0.18.0
JSON |
| download |
home_page | None |
Summary | A CLI toolkit |
upload_time | 2025-07-12 21:31:06 |
maintainer | None |
docs_url | None |
author | None |
requires_python | >=3.9 |
license | None |
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
[](https://pypi.python.org/pypi/piou)
[](https://pypi.python.org/pypi/piou)
[](https://app.circleci.com/pipelines/github/Andarius/piou?branch=master)
[](https://anaconda.org/conda-forge/piou)
A CLI tool to build beautiful command-line interfaces with type validation.
- [Why Piou](#why-piou)
- [Installation](#installation)
- [Features](#features)
- [Commands](#commands)
- [Without Command](#without-command)
- [Command Groups / Sub-commands](#command-groups--sub-commands)
- [Options processor](#options-processor)
- [Derived Options](#derived-options)
- [On Command Run](#on-command-run)
- [Help / Errors Formatter](#help--errors-formatter)
- [Moving from argparse](#moving-from-argparse)
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 | None = Option(None, '--foo', help='Foo keyword argument'),
):
"""
A longer description on what the function is doing.
You can run it with:
```bash
python -m piou.example.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 examples.simple -h`

- `python -m examples.simple foo -h`

## Why Piou?
I could not find a library that provided:
- the same developer experience as [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 in 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](#help--errors-formatter).
## Installation
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 | None = 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 | None = 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
```
### Without Command
If you want to run a function without specifying a command, you can use the `main` decorator
or the `is_main` parameter to the `command` decorator:
```python
@cli.command(help='Run without command', is_main=True)
def run_main():
pass
```
or even simpler:
```python
@cli.main()
def run_main():
pass
```
This will allow you to run the function without specifying a command:
```bash
python -m examples.simple_main -h
```
**Note**: You can only have one `main` function in the CLI.
### 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:

### 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, set:
```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 commands and group them into a single output to pass to
the command. For instance, you might want to group connection string parameters 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:

### On Command Run
If you want to get the command name and arguments information that are passed to it (for 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 of 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')):
...
```
## 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_sensitive=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": null,
"name": "piou",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "cli",
"author": null,
"author_email": "Julien Brayere <julien.brayere@obitrain.com>",
"download_url": "https://files.pythonhosted.org/packages/94/4b/18d6e6f71be4de9d049a256ce43f7ad540f4aa7130c98b0b8e9210ca746c/piou-0.18.0.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[](https://pypi.python.org/pypi/piou)\n[](https://pypi.python.org/pypi/piou)\n[](https://app.circleci.com/pipelines/github/Andarius/piou?branch=master)\n[](https://anaconda.org/conda-forge/piou)\n\n\nA CLI tool to build beautiful command-line interfaces with type validation.\n\n- [Why Piou](#why-piou)\n- [Installation](#installation)\n- [Features](#features)\n - [Commands](#commands)\n - [Without Command](#without-command)\n - [Command Groups / Sub-commands](#command-groups--sub-commands)\n - [Options processor](#options-processor)\n - [Derived Options](#derived-options)\n - [On Command Run](#on-command-run)\n - [Help / Errors Formatter](#help--errors-formatter)\n- [Moving from argparse](#moving-from-argparse)\n\nIt is as simple as\n\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 | None = 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 python -m piou.example.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 examples.simple -h`\n\n\n\n- `python -m examples.simple foo -h`\n\n\n\n\n## Why Piou?\n\nI could not find a library that provided:\n\n- the same developer experience as [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 in 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](#help--errors-formatter).\n\n## Installation\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', 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 | None = Option(None, '-g', '--foo3', help='Foo3 arguments'),\n):\n pass\n\n\n@cli.command(cmd='bar', 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 | None = 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### Without Command\n\nIf you want to run a function without specifying a command, you can use the `main` decorator\nor the `is_main` parameter to the `command` decorator:\n\n```python\n@cli.command(help='Run without command', is_main=True)\ndef run_main():\n pass\n```\nor even simpler:\n\n```python\n@cli.main()\ndef run_main():\n pass\n```\n\nThis will allow you to run the function without specifying a command:\n\n```bash\npython -m examples.simple_main -h\n```\n\n**Note**: You can only have one `main` function in the CLI.\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\n\n\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, set:\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 commands and group them into a single output to pass to\nthe command. For instance, you might want to group connection string parameters 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 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\n\n\n### On Command Run\n\nIf you want to get the command name and arguments information that are passed to it (for 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 of 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## 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_sensitive=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')`",
"bugtrack_url": null,
"license": null,
"summary": "A CLI toolkit",
"version": "0.18.0",
"project_urls": null,
"split_keywords": [
"cli"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "f2e3e141cfc3de6d054ac523f4bee0565037829eb27aa8486d6e2164ad6c8e88",
"md5": "8c3d0d7afae2bf28b5a96052a9a7fbf8",
"sha256": "1bacfbefdabe4f7c905831c74074142829b46ae4993abf70dcd243a3f2933c5d"
},
"downloads": -1,
"filename": "piou-0.18.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "8c3d0d7afae2bf28b5a96052a9a7fbf8",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 22875,
"upload_time": "2025-07-12T21:31:05",
"upload_time_iso_8601": "2025-07-12T21:31:05.713881Z",
"url": "https://files.pythonhosted.org/packages/f2/e3/e141cfc3de6d054ac523f4bee0565037829eb27aa8486d6e2164ad6c8e88/piou-0.18.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "944b18d6e6f71be4de9d049a256ce43f7ad540f4aa7130c98b0b8e9210ca746c",
"md5": "2ec1905a92bbab2ffbc9c4d78de53cbf",
"sha256": "781af0eb5d1b08706ffaa585dba81840c15f81df016f7ea5c77ff6a779ac728a"
},
"downloads": -1,
"filename": "piou-0.18.0.tar.gz",
"has_sig": false,
"md5_digest": "2ec1905a92bbab2ffbc9c4d78de53cbf",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 362187,
"upload_time": "2025-07-12T21:31:06",
"upload_time_iso_8601": "2025-07-12T21:31:06.939143Z",
"url": "https://files.pythonhosted.org/packages/94/4b/18d6e6f71be4de9d049a256ce43f7ad540f4aa7130c98b0b8e9210ca746c/piou-0.18.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-12 21:31:06",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "piou"
}