Metadata-Version: 2.4
Name: verys-py-client
Version: 0.1.0
Summary: Synchronous and asynchronous OAuth2/OIDC clients for the Verys auth service.
Author: Reis McMillan
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: cryptography>=49.0.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pyjwt>=2.13.0
Requires-Dist: starlette>=1.3.1
Description-Content-Type: text/markdown

# Verys Python Client

Synchronous and asynchronous OAuth2/OIDC clients for the Verys auth service.

## Features

- **`VerysClient`** (sync) and **`AsyncVerysClient`** (async) with identical APIs
- Authorization code flow, with **PKCE** automatically used for public clients
- Refresh token and token-exchange grants
- Federated external token retrieval
- Typed, documented public surface with a clear exception hierarchy

## Installation

```bash
uv add verys-py-client
# or
pip install verys-py-client
```

Requires Python 3.14+.

## Quickstart

### Confidential client (with a client secret)

```python
from verys_py_client import VerysClient

client = VerysClient(
    host="https://app.example.com",
    base_url="https://auth.verys.example.com",
    client_id="your-client-id",
    client_secret="your-client-secret",
    scopes=["openid", "profile", "email"],
)

# 1. Redirect the user to the authorization URL.
url, state, nonce, code_verifier = client.create_auth_url()
# Persist `state`, `nonce` (and `code_verifier` for public clients) in the session,
# then redirect the user to `url`.

# 2. Handle the callback (e.g. in a Starlette/FastAPI route).
state, code = VerysClient.handle_callback(request)

# 3. Exchange the code for tokens.
id_token, access_token, refresh_token = client.token_by_code(code, nonce)
```

### Public client (no secret — PKCE)

Construct the client with `client_secret=None`. `create_auth_url()` then attaches a
PKCE `code_challenge` and returns a `code_verifier` you must persist and pass back to
`token_by_code`:

```python
client = VerysClient(
    host="https://app.example.com",
    base_url="https://auth.verys.example.com",
    client_id="your-public-client-id",
    client_secret=None,
    scopes=["openid", "profile"],
)

url, state, nonce, code_verifier = client.create_auth_url()
# ... redirect, then on callback:
id_token, access_token, refresh_token = client.token_by_code(code, nonce, code_verifier)
```

### Other grants

```python
# Refresh an access token.
access_token, refresh_token = client.refresh_access_token(refresh_token)

# Exchange a token for one scoped to another audience.
exchanged = client.token_by_exchange(access_token, audience="https://api.other.example")

# Retrieve federated external tokens.
tokens = client.get_external_tokens(access_token)
one = client.get_external_tokens(access_token, token_id="github")
```

### Async usage

`AsyncVerysClient` mirrors the sync API; methods are awaitable:

```python
from verys_py_client import AsyncVerysClient

client = AsyncVerysClient(
    host="https://app.example.com",
    base_url="https://auth.verys.example.com",
    client_id="your-client-id",
    client_secret="your-client-secret",
    scopes=["openid", "profile"],
)

id_token, access_token, refresh_token = await client.token_by_code(code, nonce)
```

## Error handling

All errors derive from `Exception`; most application-level failures subclass
`VerysError`, while JWKS/public-key failures form a separate `JwksError` branch.

| Exception | Raised when |
| --- | --- |
| `VerysError` | Base class for client errors |
| `CallbackError` | The authorization callback reports an error |
| `MissingCodeError` / `MissingStateError` | The callback is missing `code` / `state` |
| `TokenError` | A token request fails or a returned token is invalid |
| `NonceError` | The ID token's nonce does not match |
| `ExternalReauthError` | A federated token requires re-authorization (401) |
| `JwksError` | The JWKS document cannot be retrieved |
| `PublicKeyNotFoundError` | No EdDSA signing key is present in the JWKS |

```python
from verys_py_client import TokenError, NonceError

try:
    id_token, access_token, refresh_token = client.token_by_code(code, nonce)
except NonceError:
    ...  # possible replay / mismatched session
except TokenError as e:
    ...  # token request failed
```

## Documentation

API reference is generated with [MkDocs](https://www.mkdocs.org/) +
[mkdocstrings](https://mkdocstrings.github.io/):

```bash
uv run mkdocs serve   # live preview at http://127.0.0.1:8000
uv run mkdocs build   # render static site to ./site
```

## Development

```bash
uv sync             # install dependencies (including dev tools)
uv run ruff check . # lint
```

## License

[MIT](LICENSE) © 2026 Reis McMillan
