I've been neglecting one of my more popular open source projects for a while. Clientele hit 200 stars last week and I figured it was a good time to review the issues, pull requests, contributions, and my original plans for version 2.0.

Most of what I wanted to implement for version 2.0.0 has now been shipped, along with a bunch of extra little bug fixes and contributions. It is fun revisiting this project from my pre-LLM days and seeing how I can apply LLM support here and there to reduce the time it takes me to add new features.

Dependency separation

One problem I had with Clientele was that it gained a lot of bloat - particularly when it transitioned from being a CLI code generator (pre-LLM era) to a full blown Django/FastAPI-scale framework for API integrations.

The appeal of the framework is that you can build neat API integrations without all the fuss of managing the HTTP layer / hydration / serialisation etc, but installing it meant you also had to install a bunch of CLI-related tools like click and rich.

So I've separated it out into:

  • clientele will just install the API framework.
  • clientele[cli] will install the requirements for running the code generator.

This should make it far nicer to install and use. I also took the opportunity to drop a lot of old code and template files that were used in the version first code generator (the original main function of clientele).

Move from HTTPX to HTTPX2.

I honestly can't say what's going on with httpx. I feel like I know the author quite well, and I understand they might feel hurt or let down by the community. I genuinely hope they are okay and getting the support they need to feel welcome and safe again in the tech community.

But I also think their actions are quite drastic, and don't give me confidence in running Clientele on httpx as the core http backend.

The community has since split two ways. httpxyz was the first major fork from httpx and quickly merged a whole bunch of pending pull requests. But then the well-respected Pydantic team launched their own fork called httpx2 which is also raising ahead with updates.

It's a busy time, but I'd prefer to be with a well established organisation as the core HTTP backend, so I am going to put my chips in with httpx2.

New HTTPbackends

On that note... the documentation has had a section on how to write your own HTTPBackend for a while.

One huge advantage with the abstraction I have built, and the popularity of certain frameworks, is that I can get LLMs to quickly produce a bunch of common backends using popular plugins.

So backends for requests, aiohttp and niquests are now officially shipped with Clientele.

You still need to install them explicitly, so installing clientele won't come with a bunch of extra http dependencies

Here is the code for using the RequestsHTTpBackend. All the others follow an identical interface.

api.py
from clientele.http.requests_backend import RequestsHTTPBackend
from clientele.api import config, client

# Use RequestsHTTPBackend
http_backend = RequestsHTTPBackend(
    base_url="https://api.example.com",
    headers={"Authorization": "Bearer my-token"},
    timeout=30.0,
    follow_redirects=True,
    verify=True,
)

cfg = config.BaseConfig(
    base_url="https://api.example.com",
    http_backend=http_backend,
)

api = client.APIClient(config=cfg)

@api.get("/users/{user_id}")
def get_user(result: dict, user_id: int) -> dict:
    return result

Drastically better typing support

Last time I worked on Clientele the new type checker ty was still new and fresh. It didn't even have plugin support. I had written a mypy plugin to handle the parameter injection paradigm that Clientele uses. Even so, Clientele's use of property injection in functions was weird for type checkers.

To explain consider this example:

api.py
from clientele import api
from pydantic import BaseModel

client = api.APIClient(base_url="https://pokeapi.co/api/v2/")

class Pokemon(BaseModel):
    name: str

@client.get("/pokemon/{id}")
def get_pokemon_name(result: Pokemon, id: int) -> str:
    return result.name
    
# call the api
get_pokemon_name(id=1)
>>> "Bulbasaur"

The problem is on the last line of code - calling the get_pokemon_name function without passing the result parameter causes a major issue for type checkers because they do not understand it is injected by the decorated function. They complain and break, saying you need to provide result - even though the decorator does.

I once again leveraged LLMs to get around this very complex issue with Python, and I am now excited to say that the result and request properties of these functions are fully supported even without the mypy plugin. They also work with pyright and ty. This also means that IDE support will work correctly - it won't offer you to option to pass result or request any longer.

One small side effect of this change is that they must be declared as the first properties of the function. I think that is a minor inconvenience given it is just pure python type hinting to make it work.

Configuration tidy up

Somewhat related to the points above about HTTPBackends: the BaseConfig model for configuring Clientele has all the httpx-specific bits removed in favour of the HTTPBackend interface pattern. It will also help to clear up what is http configuration and what is clientele-specific configuration.

This was all legacy from when there was only support for httpx directly.

Other changes

The changelog includes a host of other small changes that made it into 2.0.0 and beyond.

As always, any feature requests, bug submissions, or contributions, please checkout the github project!.