Metadata-Version: 2.4
Name: fastapi-service-client
Version: 1.0.0
Summary: Shared httpx client and BaseServiceClient for typed service-to-service HTTP calls
Project-URL: Homepage, https://github.com/mkovalev-dev/fastapi-service-client
Project-URL: Repository, https://github.com/mkovalev-dev/fastapi-service-client
Project-URL: Issues, https://github.com/mkovalev-dev/fastapi-service-client/issues
Project-URL: Changelog, https://github.com/mkovalev-dev/fastapi-service-client/blob/main/CHANGELOG.md
Author-email: Maxim Kovalev <makccom0@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: async,fastapi,http-client,httpx,microservices,pydantic
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.14
Requires-Dist: httpx>=0.28
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# fastapi-service-client

Shared `httpx` client and `BaseServiceClient` for typed service-to-service HTTP calls. Eliminates copy-pasting HTTP transport boilerplate across microservices: structured logging, typed responses via Pydantic, and a clean exception hierarchy for upstream failures.

## Installation

```bash
pip install fastapi-service-client
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add fastapi-service-client
```

## Quick start

### 1. Add settings to your app

```python
from pydantic_settings import BaseSettings
from fastapi_service_client import HttpxSettings

class AppSettings(BaseSettings):
    httpx: HttpxSettings = HttpxSettings()
```

### 2. Implement a service client

```python
from fastapi_service_client import BaseServiceClient, HttpxSettings

class CryptoServiceClient(BaseServiceClient):
    service_name = "crypto-service"

    def __init__(self, base_url: str, settings: HttpxSettings) -> None:
        super().__init__(base_url=base_url, settings=settings)

    async def get_key(self, key_id: str) -> dict:
        return await self._request_json("GET", f"/keys/{key_id}")
```

### 3. Use in your endpoint / use case

```python
client = CryptoServiceClient(
    base_url=settings.crypto_service_url,
    settings=settings.httpx,
)
result = await client.get_key("abc123")
```

## API

### `HttpxSettings`

Pydantic model to include in your app settings.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `ssl_verify` | `bool` | `True` | Verify SSL certificates. Set to `False` only for trusted internal services with self-signed certificates. |

### `BaseServiceClient`

Base class for outbound HTTP clients.

**Constructor:**

```python
BaseServiceClient(*, base_url: str = "", settings: HttpxSettings | None = None)
```

**Methods** (all protected, call from subclass):

| Method | Returns | Description |
|--------|---------|-------------|
| `_request_raw(method, url, ...)` | `httpx.Response` | Raw response |
| `_request_json(method, url, ...)` | `Any` | Parsed JSON |
| `_request_bytes(method, url, ...)` | `bytes` | Response body as bytes |
| `_request_typed(adapter, method, url, ...)` | `T` | Validated via `TypeAdapter[T]` |
| `_request_model(model, method, url, ...)` | `TModel` | Validated Pydantic model |

All methods accept:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `headers` | `dict[str, str] \| None` | `None` | Extra request headers |
| `cookies` | `dict[str, str] \| None` | `None` | Request cookies |
| `expected_status` | `int` | `200` | Raises `UpstreamResponseError` on mismatch |
| `detail_on_bad_status` | `str` | `"Upstream request failed"` | Error message on bad status |
| `**kwargs` | | | Passed through to `httpx.AsyncClient.request` |

**Class attribute:**

```python
class MyClient(BaseServiceClient):
    service_name = "my-service"  # used in exception messages and logs
```

### Exceptions

```python
from fastapi_service_client import (
    UpstreamError,               # base
    UpstreamUnavailableError,    # network error / timeout
    UpstreamResponseError,       # unexpected HTTP status
    UpstreamInvalidResponseError # unparseable response body
)
```

All inherit from `UpstreamError`. Catch the base to handle any upstream failure:

```python
try:
    result = await client.get_key("abc")
except UpstreamUnavailableError:
    raise HTTPException(status_code=503)
except UpstreamResponseError as exc:
    raise HTTPException(status_code=exc.status_code, detail=exc.detail)
```

## Logging

The library logs via standard Python `logging`. Each outbound request and response is logged at `INFO` level under the `fastapi_service_client` namespace:

```json
{"event": "http_out_request", "method": "GET", "url": "http://..."}
{"event": "http_out_response", "status": 200, "url": "http://...", "duration_ms": 42.1}
```

Configure in your app's logging setup as usual.

## Versioning

Releases are automated via [python-semantic-release](https://python-semantic-release.readthedocs.io/) on every push to `main`. Version bumps follow [Conventional Commits](https://www.conventionalcommits.org/):

| Commit prefix | Version bump |
|---------------|-------------|
| `fix:` | patch (`1.0.0` → `1.0.1`) |
| `feat:` | minor (`1.0.0` → `1.1.0`) |
| `feat!:` / `BREAKING CHANGE:` | major (`1.0.0` → `2.0.0`) |

## License

[MIT](LICENSE)
