Clientele
repository·main·Indexed 43 days ago
https://github.com/phalt/clienteleA Python framework for building robust API integrations with Pydantic-based validation, async support, and automated client scaffolding from OpenAPI and GraphQL schemas. Version 2.3.0 provides tools like APIClient for REST and GraphQLClient for GraphQL, featuring automatic response hydration and compatibility with FastAPI, Django REST Framework, and Django Ninja.
What's inside Clientele
- Clientele is a framework designed for building and maintaining API integrations. It allows developers to focus on API functionality and data structures while the framework handles the underlying HTTP complexities, such as networking, retries, and caching. It is designed to feel similar to popular Python API server frameworks and integrates deeply with Pydantic for data validation.
How Clientele API requests work
mainClientele uses decorators to define API clients. When you call a decorated function, Clientele handles the HTTP request and injects the response into your function.
Parameter Injection Rules
resultparameter (Mandatory): This parameter is injected by Clientele after the HTTP response arrives. Its type annotation (e.g.,User) determines how the response is parsed and validated. Callers never supply this argument.responseparameter (Optional): Injects thehttpx.Responseobject, useful for debugging or accessing headers.- Ordering Requirement: Both
resultandresponsemust be declared first in your function signature, before any caller-supplied parameters (path parameters, query parameters, ordata).
Request Parameters
- Path Parameters: Variables inside
{}in the URL (e.g.,{user_id}) are automatically filled from function arguments matching those names. - Query Parameters: Any remaining keyword arguments that are not path parameters or
dataare treated as query parameters. - Request Body (
data): For body-based methods (POST, PUT, PATCH, DELETE), the request body must be provided via thedatakeyword argument.datacan be a Pydantic model or aTypedDict.
@client.get("/users/{user_id}") def get_user(result: User, user_id: int, include_details: bool = True) -> User: return result # user_id is a path param, include_details is a query param user = get_user(42, include_details=True)How injected parameters work in Clientele
mainWhen using decorators like
@client.get(...), Clientele needs to provide the parsed HTTP response to your function. To do this without forcing the caller to provide the response manually, Clientele uses injected parameters.These parameters (
resultandresponse) are included in your function definition so you can use them inside the function body, but Clientele strips them from the public signature. This means callers and IDEs only see the parameters they are supposed to provide (like path or query parameters), while the injected parameters remain invisible to the caller.# What you write: def get_user(result: User, user_id: int) -> User # What your IDE and type checker see after decoration: def get_user(user_id: int) -> UserCompatibility with Python API frameworks
mainClientele is 100% compatible with OpenAPI schemas generated by the following Python frameworks:
- FastAPI
- Django REST Framework (via
drf-spectacular) - Django Ninja
Use Pydantic schemas for request and response data
mainDjango Ninja
Schemaobjects are automatically converted into PydanticBaseModelclasses within the generated client'sschemasmodule. This provides full type safety for both request payloads and response bodies.- Requests: Pass a schema instance to the
dataargument of a client method. - Responses: The returned object is typed as the corresponding schema (e.g.,
schemas.UserOut).
# In my_client/schemas.py import pydantic class UserIn(pydantic.BaseModel): username: str email: str age: int class UserOut(pydantic.BaseModel): id: int username: str email: str age: int created_at: datetime # Usage user_data = schemas.UserIn(username="alice", email="alice@example.com", age=30) response = client.create_user(data=user_data) # response is typed as schemas.UserOut print(response.id)- Requests: Pass a schema instance to the
How type-based hydration works in streams
mainClientele uses the type hint inside the
AsyncIterator[T](orIterator[T]) to determine how to parse and hydrate each line of the stream:str: No parsing is performed; each yielded item is a raw string.dict: Each line is parsed as JSON into a dictionary.Pydantic Model: Each line is parsed as JSON and validated against the provided Pydantic model.
This hydration happens automatically based on the type signature of the
resultparameter in your decorated function.from typing import AsyncIterator from pydantic import BaseModel class Token(BaseModel): text: str id: int # Using Pydantic for automatic JSON parsing and validation @client.get("/stream", streaming_response=True) async def stream_tokens(*, result: AsyncIterator[Token]) -> AsyncIterator[Token]: return resultCustomize client method names using operation_id
mainBy default, Clientele generates client method names by combining the
operation_idfrom the OpenAPI schema with the HTTP method (e.g.,list_users_api_users_get).To get cleaner, more readable method names in your Python client, define a custom
operation_idin your Django Ninja decorators. Clientele will use this exact string as the method name.Example Transformation:
- Default:
@api.get("/users")$\rightarrow$client.list_users_api_users_get() - Custom:
@api.get("/users", operation_id="list_users")$\rightarrow$client.list_users()
from ninja import NinjaAPI api = NinjaAPI() # This results in client.list_users() @api.get("/users", operation_id="list_users") def get_users(request): return [] # This results in client.create_user() @api.post("/users", operation_id="create_user") def create_user(request, user: UserIn): return user- Default:
Compare direct requests vs. decorated functions
mainClientele offers two ways to interact with APIs. Choosing between them depends on your use case.
Direct Requests (
APIClient.request)Best for: Small, ad-hoc requests.
What you get:
- Strongly typed responses
- Automatic data validation on responses
- Response hydration into Pydantic models
What you lose:
- Strongly typed inputs for parameters (inputs are passed as kwargs)
- An abstract functional interface to the API
- Support for streaming responses
Decorated Functions
Best for: Primary API integration and structured client code.
What you get:
- Strongly typed inputs
- Abstract functional interface
- Streaming response support
Mix sync and async functions in a Clientele API client
mainThe Clientele API supports both synchronous (
def) and asynchronous (async def) function definitions within the sameAPIClientinstance. You can define standard synchronous endpoints alongside asynchronous ones to suit your specific logic requirements or integration needs.from clientele import api as clientele_api from .my_config import Config from .my_models import BookResponse, CreateBookResponse, CreateBookRequest client = clientele_api.APIClient(config=Config()) # A synchronous endpoint @client.post("/books") def create_user( result: CreateBookResponse, data: CreateBookResponse, ) -> CreateBookResponse: return result # An asynchronous endpoint in the same client @client.get("/book/{book_id}") async def get_book(result: BookResponse, book_id: int) -> BookResponse: return resultCustomize client function names using operationId
mainBy default, Clientele generates function names using the pattern
{function_name}_{path}_{method}(e.g.,get_users_users_get).To get cleaner, more readable function names in your client, define a custom
operation_idin your FastAPI route decorators. Clientele will use this exact string as the method name.from fastapi import FastAPI app = FastAPI() # This will generate client.list_users() @app.get("/users", operation_id="list_users") def get_users(): return [] # This will generate client.create_user() @app.post("/users", operation_id="create_user") def create_user(user: UserCreate): return userUse the Clientele API client generator
mainThe Clientele API client generator is available for producing client implementations based on the Clientele API specification.Handle optional GraphQL variables
mainTo handle optional (nullable) GraphQL variables, use Python's
| Nonetype annotation for the function parameters.When a parameter is passed as
None,GraphQLClientwill omit the variable from the request payload entirely rather than sending anullvalue. This follows GraphQL best practices for optional variables.from pydantic import BaseModel from clientele.graphql import GraphQLClient client = GraphQLClient(base_url="https://api.github.com/graphql") class Repository(BaseModel): name: str language: str | None = None class RepositoriesQueryData(BaseModel): repositories: list[Repository] class RepositoriesQueryResponse(BaseModel): data: RepositoriesQueryData @client.query(""" query($owner: String!, $language: String) { repositories(owner: $owner, language: $language) { name language } } """) def search_repos(result: RepositoriesQueryResponse, owner: str, language: str | None) -> list[Repository]: return result.data.repositories # language is omitted from variables repos = search_repos(owner="phalt") # language is included python_repos = search_repos(owner="phalt", language="Python")