Metadata-Version: 2.4
Name: literegistry-podman-client
Version: 0.1.0
Summary: A standalone asynchronous client for Podman sessions through a LiteRegistry gateway.
Author: Goncalo Faria
License-Expression: MIT
Keywords: podman,containers,literegistry,asyncio,docker-mirror
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp<4,>=3.9
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Provides-Extra: publish
Requires-Dist: build>=1.2; extra == "publish"
Requires-Dist: twine>=5; extra == "publish"
Dynamic: license-file

# literegistry-podman-client

`literegistry-podman-client` is a small, standalone async Python client for
running commands in rootless Podman containers through a LiteRegistry gateway.
The person using it needs only a gateway URL; they do not need Redis, Podman,
Docker, or the full `literegistry` package.

The same gateway may also expose a Docker Hub pull-through mirror. Mirror use
is configured on the Podman servers by the operator, so client code still only
passes a normal image such as `docker.io/library/ubuntu:24.04`.

## Install

From PyPI after publication:

```bash
pip install literegistry-podman-client
```

From this repository:

```bash
pip install ./literegistry_podman_client
```

The distribution name uses hyphens. Python imports use underscores:

```python
from literegistry_podman_client import PodmanGatewayClient
```

Its only runtime dependency is `aiohttp`.

## Give a deployment to someone

The operator gives the user one value:

```bash
export PODMAN_GATEWAY_URL=http://gateway.example:8080
```

The user can verify both gateway features:

```bash
curl -fsS "$PODMAN_GATEWAY_URL/health"
curl -fsS "$PODMAN_GATEWAY_URL/v2/"
```

`/health` checks the LiteRegistry gateway. `/v2/` checks its Docker Registry
V2 mirror route. No Redis URL or Podman replica address is exposed to users.

## Small async example

This creates one container, writes a file, reads it in a separate command, and
always deletes the container at the end:

```python
import asyncio

from literegistry_podman_client import PodmanGatewayClient


async def main() -> None:
    gateway_url = "http://gateway.example:8080"

    async with PodmanGatewayClient(gateway_url, workdir="/tmp") as client:
        async with client.session(
            image="docker.io/library/ubuntu:24.04",
            client_id="rollout-17",
        ) as podman:
            print(podman.container_id)

            await podman.execute(
                "printf 'ai2 hello\\n' > hello.txt",
                check=True,
            )
            result = await podman.execute("cat hello.txt", check=True)
            print(result.stdout, end="")


asyncio.run(main())
```

The image pull happens on the selected Podman replica. If the operator wired
those replicas to the gateway's mirror, the pull is transparently cached; the
user does not change the image reference or client configuration.

The runnable version is [`examples/ai2_hello.py`](examples/ai2_hello.py):

```bash
python literegistry_podman_client/examples/ai2_hello.py \
  --gateway "$PODMAN_GATEWAY_URL"
```

## Explicit lifecycle

Handshake, execute, and close are all async. The affinity ID returned by the
handshake keeps every command on the replica that owns its container.

```python
client = PodmanGatewayClient(gateway_url, workdir="/home/user")
await client.open()
session = None
try:
    session = await client.handshake(image=container_image)
    first = await client.execute(
        session.affinity_id,
        "python -c 'print(6 * 7)'",
        timeout=60,
    )
    first.check_returncode()
finally:
    try:
        if session is not None:
            await session.close()
    finally:
        await client.aclose()
```

- `client.close(affinity_id)` or `session.close()` deletes the container and
  its gateway affinity binding.
- `client.aclose()` only closes the local HTTP connection pool. It cannot
  guess which concurrent sessions should be deleted.
- `check=True` or `result.check_returncode()` raises `PodmanCommandError` for a
  non-zero command exit. Without it, stdout, stderr, and the exit code remain
  available on `CommandResult`.

## Concurrent trajectories

One `PodmanGatewayClient` is intentionally shareable. It does not store a
private "current container". Each handshake returns a separate session:

```python
async def rollout(client: PodmanGatewayClient, number: int) -> str:
    async with client.session(client_id=f"rollout-{number}") as podman:
        result = await podman.execute("echo $((20 + 22))", check=True)
        return result.stdout.strip()


async with PodmanGatewayClient(gateway_url) as client:
    outputs = await asyncio.gather(
        *(rollout(client, i) for i in range(128))
    )
```

The HTTP pool is shared, while every request carries its explicit affinity ID.
As soon as one trajectory completes, application code may start another.

## Mirror behavior

There are two distinct flows behind the gateway URL:

```text
Python client -> /affinity/* -> Podman replica -> rootless container
Podman pull   -> /v2/*       -> Docker mirror -> Docker Hub
```

The client exposes `await client.mirror_health()` as a convenience probe, but
it does not configure the mirror. The deployment operator configures each
Podman replica's `containers-registries.conf` to use the gateway URL. This is
what ensures all users of the gateway benefit from the cache.

## Build and publish

```bash
cd literegistry_podman_client
python -m build
python -m twine check dist/*
python -m twine upload dist/*
```

Publishing requires a PyPI account and token. Verify that the distribution
name `literegistry-podman-client` is available before the first upload.
