Metadata-Version: 2.4
Name: baobab-api-call
Version: 1.0.0
Summary: Librairie Python modulaire pour construire des clients d’API HTTP synchrones et asynchrones : requêtes REST, auth, retries, timeouts, JSON, erreurs, middlewares et logs. Socle générique extensible pour développer des intégrations d’APIs ou des microservices.
Project-URL: Homepage, https://github.com/baobabgit/python-baobab-api-call
Project-URL: Documentation, https://github.com/baobabgit/python-baobab-api-call#readme
Project-URL: Repository, https://github.com/baobabgit/python-baobab-api-call
Project-URL: Changelog, https://github.com/baobabgit/python-baobab-api-call/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/baobabgit/python-baobab-api-call/issues
Author: ANDRIANAIVO Patrick
License: MIT License
        
        Copyright (c) 2026 baobabgit
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: aiohttp,api,async,baobab,client,http,httpx,requests,rest
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Framework :: aiohttp
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: aiohttp<4,>=3.9
Requires-Dist: httpx<1,>=0.27
Requires-Dist: requests<3,>=2.31
Provides-Extra: dev
Requires-Dist: bandit<2,>=1.7; extra == 'dev'
Requires-Dist: black<26,>=24; extra == 'dev'
Requires-Dist: build<2,>=1; extra == 'dev'
Requires-Dist: coverage[toml]<8,>=7; extra == 'dev'
Requires-Dist: flake8-pyproject<2,>=1.2; extra == 'dev'
Requires-Dist: flake8<8,>=7; extra == 'dev'
Requires-Dist: hatchling<2,>=1.24; extra == 'dev'
Requires-Dist: mypy<2,>=1.10; extra == 'dev'
Requires-Dist: pylint<5,>=3; extra == 'dev'
Requires-Dist: pytest-asyncio<1,>=0.23; extra == 'dev'
Requires-Dist: pytest<9,>=7; extra == 'dev'
Requires-Dist: types-requests<3,>=2.31; extra == 'dev'
Description-Content-Type: text/markdown

# baobab-api-call

Socle client HTTP Python pour les appels API **Baobab** : configuration centralisée, transports interchangeables, authentification, retry, middlewares et modèles de requête/réponse immuables.

| | |
|---|---|
| **Version** | **1.0.0** ([changelog](CHANGELOG.md)) |
| **Package PyPI** | [`baobab-api-call`](https://pypi.org/project/baobab-api-call/) |
| **Python** | ≥ 3.11 |
| **Licence** | [MIT](LICENSE) |
| **Dépôt** | [github.com/baobabgit/python-baobab-api-call](https://github.com/baobabgit/python-baobab-api-call) |

---

## Sommaire

- [Fonctionnalités](#fonctionnalités)
- [Installation](#installation)
- [Démarrage rapide](#démarrage-rapide)
- [Transports HTTP](#transports-http)
- [Configuration](#configuration)
- [Authentification](#authentification)
- [Retry et résilience](#retry-et-résilience)
- [Middlewares](#middlewares)
- [Réponses HTTP](#réponses-http)
- [Exceptions](#exceptions)
- [Développement](#développement)
- [Publication et CI](#publication-et-ci)
- [Changelog](#changelog)
- [Licence](#licence)

---

## Fonctionnalités

- **Clients sync et async** avec la même logique métier (construction de requête, auth, retry, middlewares).
- **Transports pluggables** : `requests`, `httpx` (sync + async), `aiohttp` (async), `urllib` (stdlib, sync).
- **Configuration immuable** (`BaobabApiCallClientConfig`) : URL de base, en-têtes, timeouts, politique de retry.
- **Stratégies d’auth** : Basic, Bearer, clé API (en-tête ou query).
- **Retry** avec backoff exponentiel, jitter et codes HTTP / exceptions configurables.
- **Middlewares** synchrones et asynchrones (chaînes `before_request` / `after_response` / `on_error`).
- **Réponses typées** : propriétés `is_success`, `is_error`, parsing JSON, `raise_for_status()`.
- **Exceptions normalisées** : réseau, timeout, HTTP, sérialisation, retry épuisé.

---

## Installation

```bash
pip install baobab-api-call==1.0.0
```

Pour la dernière version 1.x :

```bash
pip install "baobab-api-call>=1.0,<2"
```

Dépendances installées automatiquement : **requests**, **httpx**, **aiohttp**. Le transport **urllib** utilise la bibliothèque standard (aucun paquet supplémentaire).

### Développement

```bash
git clone https://github.com/baobabgit/python-baobab-api-call.git
cd python-baobab-api-call
pip install -e ".[dev]"
```

---

## Démarrage rapide

### Client synchrone (httpx)

```python
from baobab_api_call.client.apis.baobab_api_call_sync_api_client import (
    BaobabApiCallSyncApiClient,
)
from baobab_api_call.client.config.baobab_api_call_client_config import (
    BaobabApiCallClientConfig,
)
from baobab_api_call.transports.httpx.baobab_api_call_sync_httpx_transport import (
    BaobabApiCallSyncHttpxTransport,
)

config = BaobabApiCallClientConfig(
    base_url="https://api.example.com",
    timeout_seconds=30.0,
)
transport = BaobabApiCallSyncHttpxTransport(timeout_seconds=30.0)

with BaobabApiCallSyncApiClient(config, transport) as client:
    response = client.get("/v1/items", params={"page": "1"})
    response.raise_for_status()
    items = response.json()
```

### Client asynchrone (httpx)

```python
import asyncio

from baobab_api_call.client.apis.baobab_api_call_async_api_client import (
    BaobabApiCallAsyncApiClient,
)
from baobab_api_call.client.config.baobab_api_call_client_config import (
    BaobabApiCallClientConfig,
)
from baobab_api_call.transports.httpx.baobab_api_call_async_httpx_transport import (
    BaobabApiCallAsyncHttpxTransport,
)

config = BaobabApiCallClientConfig(base_url="https://api.example.com")

async def main() -> None:
    transport = BaobabApiCallAsyncHttpxTransport(timeout_seconds=30.0)
    async with BaobabApiCallAsyncApiClient(config, transport) as client:
        response = await client.post("/v1/items", json={"name": "demo"})
        if response.is_success:
            print(response.json())

asyncio.run(main())
```

### Méthodes HTTP exposées

Les deux clients exposent :

| Méthode | Description |
|---------|-------------|
| `get` | GET |
| `post` | POST (corps `json=` ou `body=`) |
| `put` | PUT |
| `patch` | PATCH |
| `delete` | DELETE |
| `request` | Appel générique avec verbe HTTP |

Le paramètre `path` peut être un chemin relatif (fusionné avec `base_url`) ou une URL absolue `http://` / `https://`.

---

## Transports HTTP

Un **transport** implémente l’envoi réel sur le réseau. Le client reste identique ; seul le transport change.

| Transport | Module | Mode | Backend |
|-----------|--------|------|---------|
| `BaobabApiCallSyncRequestsTransport` | `transports.requests` | Sync | [requests](https://requests.readthedocs.io/) |
| `BaobabApiCallSyncHttpxTransport` | `transports.httpx` | Sync | [httpx](https://www.python-httpx.org/) |
| `BaobabApiCallAsyncHttpxTransport` | `transports.httpx` | Async | httpx |
| `BaobabApiCallAsyncAiohttpTransport` | `transports.aiohttp` | Async | [aiohttp](https://docs.aiohttp.org/) |
| `BaobabApiCallSyncUrllibTransport` | `transports.urllib` | Sync | `urllib.request` (stdlib) |

Exemple avec **requests** :

```python
from baobab_api_call.transports.requests.baobab_api_call_sync_requests_transport import (
    BaobabApiCallSyncRequestsTransport,
)

transport = BaobabApiCallSyncRequestsTransport(timeout_seconds=15.0)
```

Les transports ne lèvent pas d’exception sur un code HTTP 4xx/5xx : c’est la couche réponse (`raise_for_status`, propriétés `is_error`, etc.) qui interprète le statut.

---

## Configuration

`BaobabApiCallClientConfig` regroupe les paramètres partagés par le client :

```python
from baobab_api_call.client.config.baobab_api_call_client_config import (
    BaobabApiCallClientConfig,
)
from baobab_api_call.client.policies.baobab_api_call_retry_policy import (
    BaobabApiCallRetryPolicy,
)

config = BaobabApiCallClientConfig(
    base_url="https://api.example.com/v1",
    default_headers={"Accept": "application/json", "User-Agent": "my-app/1.0"},
    timeout_seconds=30.0,
    auth_strategy=...,  # voir section Authentification
    retry_policy=BaobabApiCallRetryPolicy(max_attempts=3),
)
```

| Champ | Rôle |
|-------|------|
| `base_url` | URL de base HTTP(S), normalisée à la construction |
| `default_headers` | En-têtes fusionnés sur chaque requête |
| `auth_strategy` | Stratégie d’authentification optionnelle |
| `timeout_seconds` | Délai global (défaut **30 s** si omis) |
| `timeouts` | Configuration fine (`BaobabApiCallTimeoutConfig`) |
| `retry_policy` | Politique de nouvelles tentatives |
| `middlewares` | Tuple de middlewares **sync** |
| `async_middlewares` | Tuple de middlewares **async** |
| `json_serializer` | Sérialiseur JSON personnalisé |
| `logging_config` | Configuration de journalisation |

---

## Authentification

Implémentez ou utilisez une stratégie conforme à `BaobabApiCallAuthStrategy`. Stratégies fournies :

### Basic HTTP

```python
from baobab_api_call.auth.baobab_api_call_basic_auth import BaobabApiCallBasicAuth

auth = BaobabApiCallBasicAuth(username="user", password="secret")
config = BaobabApiCallClientConfig(base_url="https://api.example.com", auth_strategy=auth)
```

### Bearer

```python
from baobab_api_call.auth.baobab_api_call_bearer_auth import BaobabApiCallBearerAuth

auth = BaobabApiCallBearerAuth(token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
```

### Clé API

```python
from baobab_api_call.auth.baobab_api_call_api_key_auth import BaobabApiCallApiKeyAuth

# En-tête X-API-Key (défaut)
auth = BaobabApiCallApiKeyAuth(api_key="my-key", header_name="X-API-Key")

# Ou schéma Authorization: ApiKey <clé>
auth = BaobabApiCallApiKeyAuth(api_key="my-key", scheme="ApiKey")
```

L’auth est appliquée **après** la fusion des en-têtes de la requête ; un en-tête `Authorization` existant est **remplacé** par la stratégie.

---

## Retry et résilience

`BaobabApiCallRetryPolicy` contrôle les nouvelles tentatives en cas d’échec retryable :

```python
from baobab_api_call.client.policies.baobab_api_call_retry_policy import (
    BaobabApiCallRetryPolicy,
)

policy = BaobabApiCallRetryPolicy(
    max_attempts=4,
    retryable_status_codes=frozenset({429, 500, 502, 503, 504}),
    backoff_initial=0.2,
    backoff_factor=2.0,
    backoff_max=30.0,
    jitter=True,
)
```

Par défaut, les exceptions réseau / timeout du socle et les codes `429`, `5xx` listés ci-dessus sont retryables. Après épuisement des tentatives, le client lève `BaobabApiCallRetryException`.

---

## Middlewares

Les middlewares enveloppent le transport sans modifier le client public.

**Sync** — protocole `BaobabApiCallMiddleware` :

- `before_request(request) -> request`
- `after_response(request, response) -> response`
- `on_error(request, error) -> None`

**Async** — protocole `BaobabApiCallMiddlewareAsync` (mêmes hooks, coroutines).

Enregistrement via la config :

```python
config = BaobabApiCallClientConfig(
    base_url="https://api.example.com",
    middlewares=(my_sync_middleware,),
    async_middlewares=(my_async_middleware,),
)
```

Les modèles `BaobabApiCallRequest` et `BaobabApiCallResponse` sont **immuable** : retournez une copie (`dataclasses.replace`) plutôt que de muter les champs.

---

## Réponses HTTP

`BaobabApiCallResponse` expose notamment :

| Membre | Description |
|--------|-------------|
| `status_code` | Code HTTP (100–599) |
| `content` | Corps en `bytes` |
| `text` | Corps décodé UTF-8 ou `None` |
| `headers` | Dictionnaire d’en-têtes |
| `json()` | Parse JSON (sérialiseur configurable) |
| `is_success` | 2xx |
| `is_client_error` / `is_server_error` | 4xx / 5xx |
| `is_error` | ≥ 400 |
| `raise_for_status()` | Lève `BaobabApiCallHttpException` si erreur HTTP |

```python
response = client.get("/resource/42")
if response.is_error:
    response.raise_for_status()
data = response.json()
```

---

## Exceptions

Hiérarchie principale (toutes dérivent de `BaobabApiCallException`) :

| Exception | Cas typique |
|-----------|-------------|
| `BaobabApiCallHttpException` | Erreur HTTP après `raise_for_status()` |
| `BaobabApiCallNetworkException` | Connexion, erreur transport |
| `BaobabApiCallTimeoutException` | Dépassement de délai |
| `BaobabApiCallSerializationException` | JSON invalide ou non sérialisable |
| `BaobabApiCallRetryException` | Tentatives de retry épuisées |
| `BaobabApiCallConfigException` | Configuration invalide |

---

## Développement

### Arborescence

```text
src/baobab_api_call/
├── client/          # Clients sync/async, config, retry
├── transports/      # Implémentations HTTP
├── auth/            # Stratégies d'authentification
├── middleware/      # Chaînes de middlewares
├── requests/        # Modèle de requête
├── responses/       # Modèle de réponse
├── exceptions/      # Erreurs publiques
└── utilities/       # Métadonnées, redaction

tests/baobab_api_call/   # Tests unitaires (miroir de src/)
```

### Qualité et tests

```bash
# Tests unitaires (sans réseau)
PYTHONPATH=src pytest tests/ -m "not integration"

# Couverture (seuil projet : 90 %)
PYTHONPATH=src coverage run -m pytest tests/ -m "not integration"
coverage report --fail-under=90

# Tests d'intégration (https://httpbin.org, réseau requis)
PYTHONPATH=src pytest tests/baobab_api_call/transports/ -m integration

# Linters
black --check src tests
flake8 src tests
pylint src/baobab_api_call --fail-under=8.5
mypy src/baobab_api_call
bandit -r src/baobab_api_call -ll
```

### Build local

```bash
python -m build
# Artefacts dans dist/ : wheel + sdist
```

---

## Publication et CI

Une release est déclenchée par un **tag semver** `vMAJOR.MINOR.PATCH` (ex. `v1.0.0`) via le workflow [`.github/workflows/release.yml`](.github/workflows/release.yml) :

```bash
git tag v1.0.0
git push origin v1.0.0
```

1. Contrôles qualité (black, flake8, pylint, mypy, bandit)
2. Tests unitaires + couverture ≥ 90 %
3. Build wheel / sdist (+ SBOM CycloneDX)
4. Publication PyPI (Trusted Publishing OIDC, environment `pypi`)
5. Attachement des artefacts à la GitHub Release (signature Sigstore)

Les tests d’intégration httpbin ne sont **pas** exécutés dans ce pipeline (dépendance réseau externe).

---

## Changelog

Voir [CHANGELOG.md](CHANGELOG.md) pour l’historique des versions (semver).

---

## Licence

Ce projet est distribué sous licence **[MIT](LICENSE)**.

Copyright (c) 2026 [baobabgit](https://github.com/baobabgit).
