Metadata-Version: 2.4
Name: pyrest-model-client
Version: 2.0.1
Summary: A simple, flexible Python HTTP client and API modeling toolkit built on httpx and pydantic.
Author: Avi Zaguri
License: MIT
Project-URL: Homepage, https://github.com/aviz92/pyrest-model-client
Project-URL: Repository, https://github.com/aviz92/pyrest-model-client
Project-URL: Issues, https://github.com/aviz92/pyrest-model-client/issues
Keywords: rest,development
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: colorlog>=6.10.1
Requires-Dist: custom-python-logger>=4.0.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.12.4
Requires-Dist: python-base-toolkit>=1.0.2
Provides-Extra: dev
Requires-Dist: pre-commit>=4.5.0; extra == "dev"
Requires-Dist: pytest>=9.0.1; extra == "dev"
Requires-Dist: pytest-asyncio>=1.3.0; extra == "dev"
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
Requires-Dist: pytest-plugins>=1.0.9; extra == "dev"
Requires-Dist: respx>=0.22.0; extra == "dev"
Requires-Dist: setuptools>=80.9.0; extra == "dev"
Requires-Dist: wheel>=0.45.1; extra == "dev"
Dynamic: license-file

![PyPI version](https://img.shields.io/pypi/v/pyrest-model-client)
![Python](https://img.shields.io/badge/python->=3.12-blue)
![Development Status](https://img.shields.io/badge/status-stable-green)
![Maintenance](https://img.shields.io/maintenance/yes/2026)
![PyPI](https://img.shields.io/pypi/dm/pyrest-model-client)
![License](https://img.shields.io/pypi/l/pyrest-model-client)

---

# pyrest-model-client

A simple, flexible Python HTTP client and API modeling toolkit built on top of [httpx](https://www.python-httpx.org/) and [pydantic](https://docs.pydantic.dev/). Easily integrate robust API requests and resource models into your Python projects.

---

## 🚀 Features
- **Model-driven**: Define and interact with API resources as Python classes using `BaseAPIModel`.
- **Easy HTTP Requests**: `RestApiClient` for GET, POST, PUT, PATCH, DELETE with automatic header and base URL management.
- **Async Support**: Full async/await support with `AsyncRestApiClient` for high-performance concurrent requests.
- **Automatic Endpoint Normalization**: Configurable endpoint path normalization (trailing slash handling).
- **Resource Path Integration**: Models can use their `resource_path` to generate endpoints and URLs automatically.
- **Flexible Authentication**: Support for Token and Bearer authentication via `build_header()` helper.
- **Response to Model Conversion**: `get_model_fields()` helper converts API responses to typed model instances.
- **Configurable Client**: Customizable timeout, connection pool limits, and redirect handling.
- **Type Safety**: All models use Pydantic for automatic validation and serialization.
- **Error Handling**: Automatic HTTP status error handling with `raise_for_status()`.
- **Extensible**: Easily create new models for any RESTful resource by extending `BaseAPIModel`.

---

## 📦 Installation
```bash
uv add pyrest-model-client
```

---

## 🔧 Usage

### 1. Define Your Models
```python
from pyrest_model_client.base import BaseAPIModel


class User(BaseAPIModel):
    name: str
    email: str
    resource_path: str = "user"


class Environment(BaseAPIModel):
    name: str
    resource_path: str = "environment"
```

### 2. Initialize the Client and Make Requests
```python
import os

from dotenv import load_dotenv

from pyrest_model_client import BaseAPIModel, RestApiClient, build_header, get_model_fields

load_dotenv()

TOKEN = os.getenv("TOKEN")
BASE_URL = f'{os.getenv("BASE_URL")}:{os.getenv("PORT")}'


class FirstApp(BaseAPIModel):
    name: str
    description: str | None = None
    resource_path: str = "first_app"


header = build_header(token=TOKEN)

# Use as a context manager (auto-closes the connection)
# Optionally configure timeout and connection pool limits:
#   timeout=httpx.Timeout(60.0, connect=10.0)
#   limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
with RestApiClient(base_url=BASE_URL, header=header) as client:
    # Example: Use resource_path from model
    app = FirstApp(name="My App", description="Test")
    endpoint = app.get_endpoint()           # Returns "first_app"
    full_url = app.get_resource_url(client) # Returns full URL

    # Example: Get all items (paginated) — get_model_fields returns list[FirstApp],
    # so type checkers infer the concrete subclass on every element.
    item_list: list[FirstApp] = []
    params = None
    while True:
        res = client.get("first_app", params=params)
        data = res.json()
        item_list.extend(get_model_fields(data["results"], model=FirstApp))
        if not data["next"]:
            break
        params = {"page": data["next"].split("/?page=")[-1]}

    # Example: Create a new item
    new_item = client.post("first_app", data={"name": "My App", "description": "A new app"})

    # Example: Full update
    updated_item = client.put("first_app/1", data={"name": "Updated App"})

    # Example: Partial update
    patched_item = client.patch("first_app/1", data={"description": "New description"})

    # Example: Delete an item
    client.delete("first_app/1")
```

### 3. Using Async Client
```python
import asyncio
import os

from dotenv import load_dotenv

from pyrest_model_client import AsyncRestApiClient, build_header

load_dotenv()

TOKEN = os.getenv("TOKEN")
BASE_URL = f'{os.getenv("BASE_URL")}:{os.getenv("PORT")}'


async def main() -> None:
    header = build_header(token=TOKEN)

    async with AsyncRestApiClient(base_url=BASE_URL, header=header) as client:
        data = (await client.get("first_app")).json()
        await client.post("first_app", data={"name": "Async App"})
        await client.put("first_app/1", data={"name": "Updated"})
        await client.patch("first_app/1", data={"description": "Patched"})
        await client.delete("first_app/1")


asyncio.run(main())
```

---

## 🤝 Contributing
Contributions are welcome! Please fork the repo, create a branch, and submit a pull request.

---

## 📄 License
MIT License — see [LICENSE](LICENSE) for details.
