diff --git a/CHANGELOG/index.html b/CHANGELOG/index.html index 95c6c93..2626b10 100644 --- a/CHANGELOG/index.html +++ b/CHANGELOG/index.html @@ -400,6 +400,13 @@
constants.py
has been renamed to config.py
to better reflect how it is used. It is not generated from a template like the other files.typing.Unions
types will generate as the short hand |
instead.--regen t
or -r t
to the generate
command. This is automatically added to the line in MANIFEST.md
to help.clientele
command in the root directory of your project.generate-basic
. This can be used to keep a consistent file structure for an API that does not use OpenAPI.We have tested Clientele with:
Let's build an API Client using clientele and an example OpenAPI schema.
+Let's build an API Client using clientele.
Our GitHub has a bunch of schemas that are proven to work with clientele, so let's use one of those!
Simply:
+In your project's root directory:
clientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o my_client/
Note
+The example above uses one of our test schemas, and will work if you copy/paste it!
+The -u
parameter expects a URL, you can provide a path to a file with -f
instead if you download the file.
The -o
parameter is the output directory of the generated client.
Run it now and you will see this output:
+Let's go over each file and talk about what it does.
Let's go over each file and talk about what it does
The client.py
file provides all the API functions from the OpenAPI schema.
The client.py
file provides all the API functions from the OpenAPI schema. Functions are a combination of the path and the HTTP method for those paths. So, a path with two HTTP methods will be turned into two python functions.
my_client/client.py | ||
---|---|---|
|
We can see one of the functions here, simple_request_simple_request_get
, is for a straight-forward HTTP GET request without any input arguments, and it returns a schema object.
Here is how you might use it:
from my_client import client
client.simple_request_simple_request_get()
->>> SimpleResponse(name='Paul')
+>>> SimpleResponse(name='Hello, clientele')
A more complex example is shown just below. This is for an HTTP POST method, and it requires an input property called data
that is an instance of a schema, and returns a union of responses. If the endpoint has url parameters or query parameters, they will appear as input arguments to the function alongside the data
argument.
A more complex example is shown just below.
+This is for an HTTP POST method, and it requires an input property called data
that is an instance of a schema, and returns one of many potential responses. If the endpoint has url parameters or query parameters, they will appear as input arguments to the function alongside the data
argument.
def request_data_request_data_post(
- data: schemas.RequestDataRequest,
-) -> typing.Union[schemas.RequestDataResponse, schemas.HTTPValidationError]:
+ data: schemas.RequestDataRequest
+) -> schemas.RequestDataResponse | schemas.HTTPValidationError:
"""Request Data"""
response = http.post(url="/request-data", data=data.model_dump())
@@ -702,116 +703,109 @@ POST and PUT functions
response = client.request_data_request_data_post(data=data)
>>> RequestDataResponse(your_input='Hello, world')
Clientele also supports HTTP PUT functionality in the exact same way as HTTP POST:
-from my_client import client, schemas
-
-data = schemas.RequestDataRequest(my_input="Hello, world")
-response = client.request_data_request_data_put(data=data)
->>> RequestDataResponse(your_input='Hello, world')
-
Clientele also supports the major HTTP methods PUT and DELETE in the same way.
If your endpoint takes path parameters (aka URL parameters) then clientele will turn them into parameters in the function:
-from my_client import client
-
-client.parameter_request_simple_request(your_input="gibberish")
->>> ParameterResponse(your_input='gibberish')
+from my_client import client
+
+client.parameter_request_simple_request(your_input="gibberish")
+>>> ParameterResponse(your_input='gibberish')
Query parameters will also be generated the same way. See this example for a function that takes a required query parameter.
+Note that, optional parameters that are not passed will be omitted when the URL is generated by Clientele.
Handling responses
Because we're using Pydantic to manage the input data, we get a strongly-typed response object.
-This works beautifully with the new structural pattern matching feature in Python 3.10:
-response = client.request_data_request_data_post(data=data)
-
-# Handle responses elegantly
-match response:
- case schemas.RequestDataResponse():
- # Handle valid response
- ...
- case schemas.ValidationError():
- # Handle validation error
- ...
+This works beautifully with the new structural pattern matching feature in Python 3.10 and up:
+response = client.request_data_request_data_post(data=data)
+
+# Handle responses elegantly
+match response:
+ case schemas.RequestDataResponse():
+ # Handle valid response
+ ...
+ case schemas.ValidationError():
+ # Handle validation error
+ ...
API Exceptions
-Clientele works by matching the shape of the response object with the Pydantic return types of a function. When it matches one, it generates the pydantic object and returns it.
-If the response object is an unintended one, it will not match a return type.
-In this case, the function will raise an http.APIException
.
-from my_client import client, http
-try:
- good_response = my_client.get_my_thing()
-except http.APIException as e:
- # The API got a response we didn't expect
- print(e.response.status_code)
+Clientele keeps a mapping of the paths and their potential response codes. When it gets a response code that fits into the map, it generates the pydantic object associated to it.
+If the HTTP response code is an unintended one, it will not match a return type. In this case, the function will raise an http.APIException
.
+from my_client import client, http
+try:
+ good_response = my_client.get_my_thing()
+except http.APIException as e:
+ # The API got a response code we didn't expect
+ print(e.response.status_code)
-The response
object will be attached to this exception class for later inspection.
+The response
object will be attached to this exception class for your own debugging.
Schemas
-The schemas.py
file has all the possible schemas, request and response, and even Enums, for the API.
-They are all subclassed from pydantic's BaseModel
. Here are a few examples:
-my_client/schemas.py import typing # noqa
-import pydantic # noqa
-from enum import Enum # noqa
-
-
-class ParameterResponse(pydantic.BaseModel):
- your_input: str
-
-class RequestDataRequest(pydantic.BaseModel):
- my_input: str
-
-class RequestDataResponse(pydantic.BaseModel):
- my_input: str
-
-# Enums subclass str so they serialize to JSON nicely
-class ExampleEnum(str, Enum):
- ONE = "ONE"
- TWO = "TWO"
+The schemas.py
file has all the possible schemas, request and response, and even Enums, for the API. These are taken from OpenAPI's schemas objects and turned into Python classes. They are all subclassed from pydantic's BaseModel
.
+Here are a few examples:
+
Configuration
One of the problems with auto-generated clients is that you often need to configure them, and
-if you try and regenerate the client at some point then your configuration gets wiped clean and you have to do it all over again.
-Clientele solves this problem by providing an entry point for configuration that will never be overwritten - constants.py
.
-When you first generate the project, you will see a file called constants.py
and it will offer configuration functions a bit like this:
-"""
-This file will never be updated on subsequent clientele runs.
-Use it as a space to store configuration and constants.
-
-DO NOT CHANGE THE FUNCTION NAMES
-"""
-
-
-def api_base_url() -> str:
- """
- Modify this function to provide the current api_base_url.
- """
- return "http://localhost"
+if you try and regenerate the client at some point in the future then your configuration gets wiped clean and you have to do it all over again.
+Clientele solves this problem by providing an entry point for configuration that will never be overwritten - config.py
.
+When you first generate the project, you will see a file called config.py
and it will offer configuration functions a bit like this:
+"""
+This file will never be updated on subsequent clientele runs.
+Use it as a space to store configuration and constants.
+
+DO NOT CHANGE THE FUNCTION NAMES
+"""
+
+
+def api_base_url() -> str:
+ """
+ Modify this function to provide the current api_base_url.
+ """
+ return "http://localhost"
-Subsequent runs of the generate
command will not change this file the first time is made, so you are free to modify the defaults to suit your needs, for example, if you need to source the base url of your API for different configurations, you can modify the api_base_url
function like this:
-from my_project import my_config
-
-def api_base_url() -> str:
- """
- Modify this function to provide the current api_base_url.
- """
- if my_config.debug:
- return "http://localhost:8000"
- elif my_config.production:
- return "http://my-production-url.com"
+Subsequent runs of the generate
command with --regen t
will not change this file if it exists, so you are free to modify the defaults to suit your needs.
+For example, if you need to source the base url of your API for different configurations, you can modify the api_base_url
function like this:
+from my_project import my_config
+
+def api_base_url() -> str:
+ """
+ Modify this function to provide the current api_base_url.
+ """
+ if my_config.debug:
+ return "http://localhost:8000"
+ elif my_config.production:
+ return "http://my-production-url.com"
Just keep the function names the same and you're good to go.
Authentication
@@ -822,26 +816,26 @@ Authentication
Then clientele will provide you information on the environment variables you need to set to
make this work during the generation. For example:
-Please see my_client/constants.py to set authentication variables
+
-The constants.py
file will have entry points for you to configure, for example, HTTP Bearer authentication will need the get_bearer_token
function to be updated, something like this:
-def get_bearer_token() -> str:
- """
- HTTP Bearer authentication.
- Used by many authentication methods - token, jwt, etc.
- Does not require the "Bearer" content, just the key as a string.
- """
- from os import environ
- return environ.get("MY_AUTHENTICATION_TOKEN")
+The config.py
file will have entry points for you to configure, for example, HTTP Bearer authentication will need the get_bearer_token
function to be updated, something like this:
+def get_bearer_token() -> str:
+ """
+ HTTP Bearer authentication.
+ Used by many authentication methods - token, jwt, etc.
+ Does not require the "Bearer" content, just the key as a string.
+ """
+ from os import environ
+ return environ.get("MY_AUTHENTICATION_TOKEN")
Additional headers
-If you want to pass specific headers with all requests made by the client, you can configure the additional_headers
function in constants.py
to do this.
-def additional_headers() -> dict:
- """
- Modify this function ot provide additional headers to all
- HTTP requests made by this client.
- """
- return {}
+If you want to pass specific headers with all requests made by the client, you can configure the additional_headers
function in config.py
to do this.
+def additional_headers() -> dict:
+ """
+ Modify this function ot provide additional headers to all
+ HTTP requests made by this client.
+ """
+ return {}
Please note that if you are using this with authentication headers, then authentication headers will overwrite these defaults if they keys match.
diff --git a/index.html b/index.html
index bb1f4d8..1303388 100644
--- a/index.html
+++ b/index.html
@@ -552,12 +552,13 @@ Async support
Other features
- Written entirely in Python.
-- Designed to work with FastAPI's OpenAPI schema generator.
+- Designed to work with FastAPI's and Ddrf-spectacular's OpenAPI schema generator.
- The generated client only depends on httpx and Pydantic 2.4.
- HTTP Basic and HTTP Bearer authentication support.
- Support your own configuration - we provide an entry point that will never be overwritten.
- Designed for easy testing with respx.
- API updated? Just run the same command again and check the git diff.
+- Automatically formats the generated client with black.
diff --git a/install/index.html b/install/index.html
index db8e2f5..ad16196 100644
--- a/install/index.html
+++ b/install/index.html
@@ -422,7 +422,7 @@ 🏗️ Install
Once installed you can run clientele version
to make sure you have the latest version:
diff --git a/search/search_index.json b/search/search_index.json
index 1b6d1ff..88ca2d0 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\u269c\ufe0f Clientele","text":""},{"location":"#generate-loveable-python-http-api-clients","title":"Generate loveable Python HTTP API Clients","text":"Clientele lets you generate fully-typed, pythonic HTTP API Clients using an OpenAPI schema.
It's easy to use:
# Install as a global tool - it's not a dependency!\npipx install clientele\n# Generate a client\nclientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o api_client/\n
"},{"location":"#generated-code","title":"Generated code","text":"The generated code is designed by python developers, for python developers.
It uses modern tooling and has a great developer experience.
from my_api import client, schemas\n\n# Pydantic models for inputs and outputs\ndata = schemas.RequestDataRequest(my_input=\"test\")\n\n# Easy to read client functions\nresponse = client.request_data_request_data_post(data=data)\n\n# Handle responses elegantly\nmatch response:\n case schemas.RequestDataResponse():\n # Handle valid response\n ...\n case schemas.ValidationError():\n # Handle validation error\n ...\n
The generated code is tiny - the example schema we use for documentation and testing only requires 250 lines of code and 5 files.
"},{"location":"#async-support","title":"Async support","text":"You can choose to generate either a sync or an async client - we support both:
from my_async_api import client\n\n# Async client functions\nresponse = await client.simple_request_simple_request_get()\n
"},{"location":"#other-features","title":"Other features","text":" - Written entirely in Python.
- Designed to work with FastAPI's OpenAPI schema generator.
- The generated client only depends on httpx and Pydantic 2.4.
- HTTP Basic and HTTP Bearer authentication support.
- Support your own configuration - we provide an entry point that will never be overwritten.
- Designed for easy testing with respx.
- API updated? Just run the same command again and check the git diff.
"},{"location":"CHANGELOG/","title":"Change log","text":""},{"location":"CHANGELOG/#063","title":"0.6.3","text":" - Packaged application installs in the correct location. Resolving #6
- Updated pyproject.toml to include a better selection of links.
"},{"location":"CHANGELOG/#062","title":"0.6.2","text":" - Ignore optional URL query parameters if they are
None
.
"},{"location":"CHANGELOG/#061","title":"0.6.1","text":" - Added
from __future__ import annotations
in files to help with typing evaluation. - Update to use pydantic 2.4.
- A bunch of documentation and readme updates.
- Small wording and grammar fixes.
"},{"location":"CHANGELOG/#060","title":"0.6.0","text":" - Significantly improved handling for response schemas. Responses from API endpoints now look at the HTTP status code to pick the correct response schema to generate from the HTTP json data. When regenerating, you will notice a bit more logic generated in the
http.py
file to handle this. - Significantly improved coverage of exceptions raised when trying to generate response schemas.
- Response types for a class are now sorted.
- Fixed a bug where
put
methods did not generate input data correctly.
"},{"location":"CHANGELOG/#052","title":"0.5.2","text":" - Fix pathing for
constants.py
- thanks to @matthewknight for the contribution! - Added
CONTRIBUTORS.md
"},{"location":"CHANGELOG/#051","title":"0.5.1","text":" - Support for HTTP PUT methods
- Headers objects use
exclude_unset
to avoid passing None
values as headers, which httpx does not support.
Additionally, an async test client is now included in the test suite. It has identical tests to the standard one but uses the async client instead.
"},{"location":"CHANGELOG/#050","title":"0.5.0","text":""},{"location":"CHANGELOG/#please-delete-the-constantspy-file-when-updating-to-this-version-to-have-new-features-take-affect","title":"Please delete the constants.py file when updating to this version to have new features take affect","text":" - Paths are resolved correctly when generating clients in nested directories.
additional_headers()
is now applied to every client, allowing you to set up headers for all requests made by your client. - When the client cannot match an HTTP response to a return type for the function it will now raise an
http.APIException
. This object will have the response
attached to it for inspection by the developer. MANIFEST
is now renamed to MANIFEST.md
and will include install information for Clientele, as well as information on the command used to generate the client.
"},{"location":"CHANGELOG/#044","title":"0.4.4","text":"Examples and documentation now includes a very complex example schema built using FastAPI that offers the following variations:
- Simple request / response (no input just an output)
- A request with a URL/Path parameter.
- Models with
int
, str
, list
, dict
, references to other models, enums, and list
s of other models and enums. - A request with query parameters.
- A response model that has optional parameters.
- An HTTP POST request that takes an input model.
- An HTTP POST request that takes path parameters and also an input model.
- An HTTP GET request that requires an HTTP header, and returns it.
- An HTTP GET endpoint that returns the HTTP bearer authorization token (also makes clientele generate the http authentication for this schema).
A huge test suite has been added to the CI pipeline for this project using a copy of the generated client from the schema above.
"},{"location":"CHANGELOG/#043","title":"0.4.3","text":" Enums
now inherit from str
as well so that they serialize to JSON properly. See this little nugget.
"},{"location":"CHANGELOG/#042","title":"0.4.2","text":" - Correctly use
model_rebuild
for complex schemas where there are nested schemas, his may be necessary when one of the annotations is a ForwardRef which could not be resolved during the initial attempt to build the schema. - Do not raise for status, instead attempt to return the response if it cannot match a response type.
"},{"location":"CHANGELOG/#041","title":"0.4.1","text":" - Correctly generate lists of nested schema classes
- Correctly build response schemas that are emphemeral (such as when they just return an array of other schemas, or when they have no $ref).
"},{"location":"CHANGELOG/#040","title":"0.4.0","text":" - Change install suggestion to use pipx as it works best as a global CLI tool.
- Improved support for OpenAPI 3.0.3 schemas (a test version is available in the example_openapi_specs directory).
validate
command for validating an OpenAPI schema will work with clientele. version
command for showing the current version of clientele. - Supports HTTP DELETE methods.
- Big refactor of how methods are generated to reduce duplicate code.
- Support optional header parameters in all request functions (where they are required).
- Very simple Oauth2 support - if it is discovered will set up HTTP Bearer auth for you.
- Uses
dict
and list
instead of typing.Dict
and typing.List
respectively. - Improved schema generation when schemas have $ref to other models.
"},{"location":"CHANGELOG/#032","title":"0.3.2","text":" - Minor changes to function name generation to make it more consistent.
- Optional parameters in schemas are working properly.
"},{"location":"CHANGELOG/#031","title":"0.3.1","text":" - Fixes a bug when generating HTTP Authentication schema.
- Fixes a bug when generating input classes for post functions, when the input schema doesn't exist yet.
- Generates pythonic function names in clients now, always (like
lower_case_snake_case
).
"},{"location":"CHANGELOG/#030","title":"0.3.0","text":" - Now generates a
MANIFEST
file with information about the build versions - Added a
constants.py
file to the output if one does not exist yet, which can be used to store values that you do not want to change between subsequent re-generations of the clientele client, such as the API base url. - Authentication patterns now use
constants.py
for constants values. - Removed
ipython
from package dependencies and moved to dev dependencies. - Documentation! https://phalt.github.io/clientele/
"},{"location":"CHANGELOG/#020","title":"0.2.0","text":" - Improved CLI output
- Code organisation is now sensible and not just one giant file
- Now supports an openapi spec generated from a dotnet project (
Microsoft.OpenApi.Models
) - async client support fully working
- HTTP Bearer support
- HTTP Basic support
"},{"location":"CHANGELOG/#010","title":"0.1.0","text":" - Initial version
- Mostly works with a simple FastAPI generated spec (3.0.2)
- Works with Twilio's spec (see example_openapi_specs/ directory) (3.0.1)
- Almost works with stripes
"},{"location":"compatibility/","title":"\ud83d\udcb1 Compatibility","text":""},{"location":"compatibility/#great-compatibility","title":"Great compatibility","text":"Any standard 3.0.x
implementation works very well.
We have tested Clientele with:
- FastAPI - our target audience, so 100% compatibility guaranteed.
- Microsoft's OpenAPI spec has also been battle tested and works well.
"},{"location":"compatibility/#no-compatibility","title":"No compatibility","text":"We do not support 2.x
aka \"Swagger\" - this format is quite different and deprecated.
"},{"location":"compatibility/#a-note-on-compatbility","title":"A note on compatbility","text":"When we were building Clientele, we discovered that, despite a fantastic specification, OpenAPI has a lot of poor implementations.
As pythonistas, we started with the auto-generated OpenAPI schemas provided by FastAPI, and then we branched out to large APIs like Twilio to test what we built.
Despite the effort, we still keep finding subtly different OpenAPI implementations.
Because of this we cannot guarentee 100% compatibility with an API, but we can give you a good indication of what we've tested.
"},{"location":"design/","title":"\ud83c\udfa8 Design","text":""},{"location":"design/#openapi-code-generators","title":"OpenAPI code generators","text":"Every few years we check the HTTP API landscape to see what has changed, and what new tooling is available. A part of this research is seeing how far OpenAPI client generators have come.
In the early years of OpenAPI, the \"last mile\" (i.e. - generating, and using, a client library) had a pretty poor experience:
-
The generated code was difficult to read, leading to problems when debugging the code. It was often not idiomatic to the language.
-
It was often bloated and repetitive, making changes tedious to support your specific project.
-
Also, you had to install a lot of things like Java to generate the code.
-
And finally, some API services still weren't publishing an OpenAPI schema. Or, the schema they published was different from the standard, so would not work with code generators.
This experience wasn't ideal, and was often such an impedance that it put us off using them. We would prefer instead to take a template of our own choosing, refined from years of working with HTTP APIs, and adapt it to whatever new API we were consuming.
"},{"location":"design/#the-landscape-in-2023","title":"The landscape in 2023","text":"In the early part of 2023, we had to build an integration with a new HTTP API. So, like we did in the past, we used it as an opportunity to asses the landscape of OpenAPI client generators.
And this was our summary at the time of writing:
-
API tools and providers had adopted OpenAPI very well. For example - tools like FastAPI and drf-spectacular now make it easy for the most popular python web frameworks to publish OpenAPI schemas.
-
There are a lot of options for generating clients. They all meet the need of \"generating a python client using the schema\". But, almost universally, they have a poor developer experience.
After evaluating many python client generators, we opted to use none of them and hand craft the API client ourselves. We used the OpenAPI schema as a source to develop the input and output objects. Then we wrote a small functional abstraction over the paths.
Looking back over our organised, pythonic, minimal client integration, we had an idea:
If this is the type of client we would like a generator to produce; how hard could it be to work backwards and build one?
This was the start of Clientele.
"},{"location":"design/#clientele","title":"Clientele","text":"As python developers ourselves, we know what makes good, readable, idiomatic python.
We also feel like we have a grasp on the best tools to be using in our python projects.
By starting with a client library that we like to use, and working backwards, we were able to build a client generator that produced something that python developers actually wanted.
But what is it exactly that we aimed to do with Clientele, why is this the OpenAPI Python client that you should use?
"},{"location":"design/#strongly-typed-inputs-and-outputs","title":"Strongly-typed inputs and outputs","text":"OpenAPI prides itself on being able to describe the input and output objects in it's schema.
This means you can build strongly-typed interfaces to the API. This helps to solve some common niggles when using an API - such as casting a value to a string when it should be an integer.
With Clientele, we opted to use Pydantic to build the models from the OpenAPI schema.
Pydantic doesn't only describe the shape of an object, it also validates the attributes as well. If an API sends back the wrong attributes at run time, Pydantic will error and provide a detail description about what went wrong.
"},{"location":"design/#idiomatic-python","title":"Idiomatic Python","text":"A lot of the client generators we tested produced a lot of poor code.
It was clear in a few cases that the client generator was built without understanding or knowledge of good python conventions.
In more than one case we also discovered the client generator would work by reading a file at run time. This is a very cool piece of engineering, but it is impractical to use. When you develop with these clients, the available functions and objects don't exist and you can't use an IDE's auto-complete feature.
Clientele set out to be as pythonic as possible. We use modern tools, idiomatic conventions, and provide some helpful bonuses like Black auto-formatting.
"},{"location":"design/#easy-to-understand","title":"Easy to understand","text":"Eventually a developer will need to do some debugging, and sometimes they'll need to do it in the generated client code.
A lot of the other client generators make obscure or obtuse code that is hard to pick apart and debug.
Now, there is a suggestion that developers shouldn't have to look at this, and that is fair. But the reality doesn't match that expectation. Personally; we like to know what generated code is doing. We want to trust that it will work, and that we can adapt around it if needs be. An interesting quirk of any code generator is you can't always inspect the source when evaluating to use it. Any other tool - you'd just go to GitHub and have a read around, but you can't with code generators.
So the code that is generated needs to be easy to understand.
Clientele doesn't do any tricks, or magic, or anything complex. The generated code has documentation and is designed to be readable. It is only a small layer on top of already well established tools, such as HTTPX.
In fact, we have some generated clients in our project repository so you can see what it looks like. We even have example tests for you to learn how to integrate with it.
It is that way because we know you will need to inspect it in the future. We want you to know that this is a sane and sensible tool.
"},{"location":"examples/","title":"\ud83e\ude84 Client example","text":"Let's build an API Client using clientele and an example OpenAPI schema.
Our GitHub has a bunch of schemas that are proven to work with clientele, so let's use one of those!
"},{"location":"examples/#generate-the-client","title":"Generate the client","text":"Simply:
clientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o my_client/\n
The -u
parameter expects a URL, you can provide a path to a file with -f
instead if you download the file.
The -o
parameter is the output directory of the generated client.
Run it now and you will see this output:
my_client/\n __init__.py\n client.py\n constants.py\n http.py\n MANIFEST\n schemas.py\n
"},{"location":"examples/#client","title":"Client","text":"Let's go over each file and talk about what it does
"},{"location":"examples/#get-functions","title":"GET functions","text":"The client.py
file provides all the API functions from the OpenAPI schema.
my_client/client.pyimport typing # noqa\nfrom . import schemas # noqa\nfrom . import http # noqa\n\n\ndef simple_request_simple_request_get() -> schemas.SimpleResponse:\n \"\"\"Simple Request\"\"\"\n\n response = http.get(url=\"/simple-request\")\n return http.handle_response(simple_request_simple_request_get, response)\n\n...\n
We can see one of the functions here, simple_request_simple_request_get
, is for a straight-forward HTTP GET request without any input arguments, and it returns a schema object.
Here is how you might use it:
from my_client import client\n\nclient.simple_request_simple_request_get()\n>>> SimpleResponse(name='Paul')\n
"},{"location":"examples/#post-and-put-functions","title":"POST and PUT functions","text":"A more complex example is shown just below. This is for an HTTP POST method, and it requires an input property called data
that is an instance of a schema, and returns a union of responses. If the endpoint has url parameters or query parameters, they will appear as input arguments to the function alongside the data
argument.
def request_data_request_data_post(\n data: schemas.RequestDataRequest,\n) -> typing.Union[schemas.RequestDataResponse, schemas.HTTPValidationError]:\n \"\"\"Request Data\"\"\"\n\n response = http.post(url=\"/request-data\", data=data.model_dump())\n return http.handle_response(request_data_request_data_post, response)\n
Here is how you might use it:
from my_client import client, schemas\n\ndata = schemas.RequestDataRequest(my_input=\"Hello, world\")\nresponse = client.request_data_request_data_post(data=data)\n>>> RequestDataResponse(your_input='Hello, world')\n
Clientele also supports HTTP PUT functionality in the exact same way as HTTP POST:
from my_client import client, schemas\n\ndata = schemas.RequestDataRequest(my_input=\"Hello, world\")\nresponse = client.request_data_request_data_put(data=data)\n>>> RequestDataResponse(your_input='Hello, world')\n
"},{"location":"examples/#url-and-query-parameters","title":"URL and Query parameters","text":"If your endpoint takes path parameters (aka URL parameters) then clientele will turn them into parameters in the function:
from my_client import client\n\nclient.parameter_request_simple_request(your_input=\"gibberish\")\n>>> ParameterResponse(your_input='gibberish')\n
Query parameters will also be generated the same way. See this example for a function that takes a required query parameter.
"},{"location":"examples/#handling-responses","title":"Handling responses","text":"Because we're using Pydantic to manage the input data, we get a strongly-typed response object. This works beautifully with the new structural pattern matching feature in Python 3.10:
response = client.request_data_request_data_post(data=data)\n\n# Handle responses elegantly\nmatch response:\n case schemas.RequestDataResponse():\n # Handle valid response\n ...\n case schemas.ValidationError():\n # Handle validation error\n ...\n
"},{"location":"examples/#api-exceptions","title":"API Exceptions","text":"Clientele works by matching the shape of the response object with the Pydantic return types of a function. When it matches one, it generates the pydantic object and returns it.
If the response object is an unintended one, it will not match a return type.
In this case, the function will raise an http.APIException
.
from my_client import client, http\ntry:\n good_response = my_client.get_my_thing()\nexcept http.APIException as e:\n # The API got a response we didn't expect\n print(e.response.status_code)\n
The response
object will be attached to this exception class for later inspection.
"},{"location":"examples/#schemas","title":"Schemas","text":"The schemas.py
file has all the possible schemas, request and response, and even Enums, for the API.
They are all subclassed from pydantic's BaseModel
. Here are a few examples:
my_client/schemas.pyimport typing # noqa\nimport pydantic # noqa\nfrom enum import Enum # noqa\n\n\nclass ParameterResponse(pydantic.BaseModel):\n your_input: str\n\nclass RequestDataRequest(pydantic.BaseModel):\n my_input: str\n\nclass RequestDataResponse(pydantic.BaseModel):\n my_input: str\n\n# Enums subclass str so they serialize to JSON nicely\nclass ExampleEnum(str, Enum):\n ONE = \"ONE\"\n TWO = \"TWO\"\n
"},{"location":"examples/#configuration","title":"Configuration","text":"One of the problems with auto-generated clients is that you often need to configure them, and if you try and regenerate the client at some point then your configuration gets wiped clean and you have to do it all over again.
Clientele solves this problem by providing an entry point for configuration that will never be overwritten - constants.py
.
When you first generate the project, you will see a file called constants.py
and it will offer configuration functions a bit like this:
\"\"\"\nThis file will never be updated on subsequent clientele runs.\nUse it as a space to store configuration and constants.\n\nDO NOT CHANGE THE FUNCTION NAMES\n\"\"\"\n\n\ndef api_base_url() -> str:\n \"\"\"\n Modify this function to provide the current api_base_url.\n \"\"\"\n return \"http://localhost\"\n
Subsequent runs of the generate
command will not change this file the first time is made, so you are free to modify the defaults to suit your needs, for example, if you need to source the base url of your API for different configurations, you can modify the api_base_url
function like this:
from my_project import my_config\n\ndef api_base_url() -> str:\n \"\"\"\n Modify this function to provide the current api_base_url.\n \"\"\"\n if my_config.debug:\n return \"http://localhost:8000\"\n elif my_config.production:\n return \"http://my-production-url.com\"\n
Just keep the function names the same and you're good to go.
"},{"location":"examples/#authentication","title":"Authentication","text":"If your OpenAPI spec provides security information for the following authentication methods:
- HTTP Bearer
- HTTP Basic
Then clientele will provide you information on the environment variables you need to set to make this work during the generation. For example:
Please see my_client/constants.py to set authentication variables\n
The constants.py
file will have entry points for you to configure, for example, HTTP Bearer authentication will need the get_bearer_token
function to be updated, something like this:
def get_bearer_token() -> str:\n \"\"\"\n HTTP Bearer authentication.\n Used by many authentication methods - token, jwt, etc.\n Does not require the \"Bearer\" content, just the key as a string.\n \"\"\"\n from os import environ\n return environ.get(\"MY_AUTHENTICATION_TOKEN\")\n
"},{"location":"examples/#additional-headers","title":"Additional headers","text":"If you want to pass specific headers with all requests made by the client, you can configure the additional_headers
function in constants.py
to do this.
def additional_headers() -> dict:\n \"\"\"\n Modify this function ot provide additional headers to all\n HTTP requests made by this client.\n \"\"\"\n return {}\n
Please note that if you are using this with authentication headers, then authentication headers will overwrite these defaults if they keys match.
"},{"location":"install/","title":"\ud83c\udfd7\ufe0f Install","text":"We recommend installing with pipx as a global CLI command:
pipx install clientele\n
Once installed you can run clientele version
to make sure you have the latest version:
> clientele version\nclientele 0.6.3\n
"},{"location":"testing/","title":"Testing","text":"Clientele is designed for easy testing, and our own test suite is a great example of how easily you can write mock tests for your API Client.
import pytest\nfrom httpx import Response\nfrom respx import MockRouter\n\nfrom .test_client import client, constants, schemas\n\nBASE_URL = constants.api_base_url()\n\n\n@pytest.mark.respx(base_url=BASE_URL)\ndef test_simple_request_simple_request_get(respx_mock: MockRouter):\n # Given\n mocked_response = {\"status\": \"hello world\"}\n mock_path = \"/simple-request\"\n respx_mock.get(mock_path).mock(\n return_value=Response(json=mocked_response, status_code=200)\n )\n # When\n response = client.simple_request_simple_request_get()\n # Then\n assert isinstance(response, schemas.SimpleResponse)\n assert len(respx_mock.calls) == 1\n call = respx_mock.calls[0]\n assert call.request.url == BASE_URL + mock_path\n
We recommend you install respx for writing your tests.
"},{"location":"usage/","title":"\ud83d\udcdd Use Clientele","text":"Note
You can type clientele COMMAND --help
at anytime to see explicit information about the available arguments.
"},{"location":"usage/#generate","title":"generate
","text":"Generate a Python HTTP Client from an OpenAPI Schema.
"},{"location":"usage/#from-a-url","title":"From a URL","text":"Use the -u
or --url
argument.
-o
or --output
is the target directory for the generate client.
clientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o my_client/\n
Note
The example above uses one of our test schemas, and will work if you copy/paste it!
"},{"location":"usage/#from-a-file","title":"From a file","text":"Alternatively you can provide a local file using the -f
or --file
argument.
clientele generate -f path/to/file.json -o my_client/\n
"},{"location":"usage/#asyncio","title":"Async.io","text":"If you prefer an asyncio client, just pass --asyncio t
to your command.
clientele generate -f path/to/file.json -o my_client/ --asyncio t\n
"},{"location":"usage/#validate","title":"validate
","text":"Validate lets you check if an OpenAPI schema will work with clientele.
Note
Some OpenAPI schema generators do not conform to the specification.
Clientele uses openapi-core to validate the schema.
"},{"location":"usage/#from-a-url_1","title":"From a URL","text":"Use the -u
or --url
argument.
-o
or --output
is the target directory for the generate client.
clientele validate -u http://path.com/to/openapi.json\n
"},{"location":"usage/#from-a-file-path","title":"From a file path","text":"Alternatively you can provide a local file using the -f
or --file
argument.
clientele validate -f /path/to/openapi.json\n
"},{"location":"usage/#version","title":"version
","text":"Print the current version of Clientele:
> clientele version\nClientele 0.7.0\n
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"\u269c\ufe0f Clientele","text":""},{"location":"#generate-loveable-python-http-api-clients","title":"Generate loveable Python HTTP API Clients","text":"Clientele lets you generate fully-typed, pythonic HTTP API Clients using an OpenAPI schema.
It's easy to use:
# Install as a global tool - it's not a dependency!\npipx install clientele\n# Generate a client\nclientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o api_client/\n
"},{"location":"#generated-code","title":"Generated code","text":"The generated code is designed by python developers, for python developers.
It uses modern tooling and has a great developer experience.
from my_api import client, schemas\n\n# Pydantic models for inputs and outputs\ndata = schemas.RequestDataRequest(my_input=\"test\")\n\n# Easy to read client functions\nresponse = client.request_data_request_data_post(data=data)\n\n# Handle responses elegantly\nmatch response:\n case schemas.RequestDataResponse():\n # Handle valid response\n ...\n case schemas.ValidationError():\n # Handle validation error\n ...\n
The generated code is tiny - the example schema we use for documentation and testing only requires 250 lines of code and 5 files.
"},{"location":"#async-support","title":"Async support","text":"You can choose to generate either a sync or an async client - we support both:
from my_async_api import client\n\n# Async client functions\nresponse = await client.simple_request_simple_request_get()\n
"},{"location":"#other-features","title":"Other features","text":" - Written entirely in Python.
- Designed to work with FastAPI's and Ddrf-spectacular's OpenAPI schema generator.
- The generated client only depends on httpx and Pydantic 2.4.
- HTTP Basic and HTTP Bearer authentication support.
- Support your own configuration - we provide an entry point that will never be overwritten.
- Designed for easy testing with respx.
- API updated? Just run the same command again and check the git diff.
- Automatically formats the generated client with black.
"},{"location":"CHANGELOG/","title":"Change log","text":""},{"location":"CHANGELOG/#070","title":"0.7.0","text":" - Updated all files to use the templates engine.
- Generator files have been reorganised in clientele to support future templates.
constants.py
has been renamed to config.py
to better reflect how it is used. It is not generated from a template like the other files. - If you are using Python 3.10 or later, the
typing.Unions
types will generate as the short hand |
instead. - To regenerate a client (and to prevent accidental overrides) you must now pass
--regen t
or -r t
to the generate
command. This is automatically added to the line in MANIFEST.md
to help. - Clientele will now automatically run black code formatter once a client is generated or regenerated.
- Clientele will now generate absolute paths to refer to adjacent files in the generated client, instead of relative paths. This assumes you are running the
clientele
command in the root directory of your project. - A lot of documentation and docs strings updates so that code in the generated client is easier to understand.
- Improved the utility for snake-casing enum keys. Tests added for the functions.
- Python 3.12 support.
- Add a \"basic\" client using the command
generate-basic
. This can be used to keep a consistent file structure for an API that does not use OpenAPI.
"},{"location":"CHANGELOG/#063","title":"0.6.3","text":" - Packaged application installs in the correct location. Resolving #6
- Updated pyproject.toml to include a better selection of links.
"},{"location":"CHANGELOG/#062","title":"0.6.2","text":" - Ignore optional URL query parameters if they are
None
.
"},{"location":"CHANGELOG/#061","title":"0.6.1","text":" - Added
from __future__ import annotations
in files to help with typing evaluation. - Update to use pydantic 2.4.
- A bunch of documentation and readme updates.
- Small wording and grammar fixes.
"},{"location":"CHANGELOG/#060","title":"0.6.0","text":" - Significantly improved handling for response schemas. Responses from API endpoints now look at the HTTP status code to pick the correct response schema to generate from the HTTP json data. When regenerating, you will notice a bit more logic generated in the
http.py
file to handle this. - Significantly improved coverage of exceptions raised when trying to generate response schemas.
- Response types for a class are now sorted.
- Fixed a bug where
put
methods did not generate input data correctly.
"},{"location":"CHANGELOG/#052","title":"0.5.2","text":" - Fix pathing for
constants.py
- thanks to @matthewknight for the contribution! - Added
CONTRIBUTORS.md
"},{"location":"CHANGELOG/#051","title":"0.5.1","text":" - Support for HTTP PUT methods
- Headers objects use
exclude_unset
to avoid passing None
values as headers, which httpx does not support.
Additionally, an async test client is now included in the test suite. It has identical tests to the standard one but uses the async client instead.
"},{"location":"CHANGELOG/#050","title":"0.5.0","text":""},{"location":"CHANGELOG/#please-delete-the-constantspy-file-when-updating-to-this-version-to-have-new-features-take-affect","title":"Please delete the constants.py file when updating to this version to have new features take affect","text":" - Paths are resolved correctly when generating clients in nested directories.
additional_headers()
is now applied to every client, allowing you to set up headers for all requests made by your client. - When the client cannot match an HTTP response to a return type for the function it will now raise an
http.APIException
. This object will have the response
attached to it for inspection by the developer. MANIFEST
is now renamed to MANIFEST.md
and will include install information for Clientele, as well as information on the command used to generate the client.
"},{"location":"CHANGELOG/#044","title":"0.4.4","text":"Examples and documentation now includes a very complex example schema built using FastAPI that offers the following variations:
- Simple request / response (no input just an output)
- A request with a URL/Path parameter.
- Models with
int
, str
, list
, dict
, references to other models, enums, and list
s of other models and enums. - A request with query parameters.
- A response model that has optional parameters.
- An HTTP POST request that takes an input model.
- An HTTP POST request that takes path parameters and also an input model.
- An HTTP GET request that requires an HTTP header, and returns it.
- An HTTP GET endpoint that returns the HTTP bearer authorization token (also makes clientele generate the http authentication for this schema).
A huge test suite has been added to the CI pipeline for this project using a copy of the generated client from the schema above.
"},{"location":"CHANGELOG/#043","title":"0.4.3","text":" Enums
now inherit from str
as well so that they serialize to JSON properly. See this little nugget.
"},{"location":"CHANGELOG/#042","title":"0.4.2","text":" - Correctly use
model_rebuild
for complex schemas where there are nested schemas, his may be necessary when one of the annotations is a ForwardRef which could not be resolved during the initial attempt to build the schema. - Do not raise for status, instead attempt to return the response if it cannot match a response type.
"},{"location":"CHANGELOG/#041","title":"0.4.1","text":" - Correctly generate lists of nested schema classes
- Correctly build response schemas that are emphemeral (such as when they just return an array of other schemas, or when they have no $ref).
"},{"location":"CHANGELOG/#040","title":"0.4.0","text":" - Change install suggestion to use pipx as it works best as a global CLI tool.
- Improved support for OpenAPI 3.0.3 schemas (a test version is available in the example_openapi_specs directory).
validate
command for validating an OpenAPI schema will work with clientele. version
command for showing the current version of clientele. - Supports HTTP DELETE methods.
- Big refactor of how methods are generated to reduce duplicate code.
- Support optional header parameters in all request functions (where they are required).
- Very simple Oauth2 support - if it is discovered will set up HTTP Bearer auth for you.
- Uses
dict
and list
instead of typing.Dict
and typing.List
respectively. - Improved schema generation when schemas have $ref to other models.
"},{"location":"CHANGELOG/#032","title":"0.3.2","text":" - Minor changes to function name generation to make it more consistent.
- Optional parameters in schemas are working properly.
"},{"location":"CHANGELOG/#031","title":"0.3.1","text":" - Fixes a bug when generating HTTP Authentication schema.
- Fixes a bug when generating input classes for post functions, when the input schema doesn't exist yet.
- Generates pythonic function names in clients now, always (like
lower_case_snake_case
).
"},{"location":"CHANGELOG/#030","title":"0.3.0","text":" - Now generates a
MANIFEST
file with information about the build versions - Added a
constants.py
file to the output if one does not exist yet, which can be used to store values that you do not want to change between subsequent re-generations of the clientele client, such as the API base url. - Authentication patterns now use
constants.py
for constants values. - Removed
ipython
from package dependencies and moved to dev dependencies. - Documentation! https://phalt.github.io/clientele/
"},{"location":"CHANGELOG/#020","title":"0.2.0","text":" - Improved CLI output
- Code organisation is now sensible and not just one giant file
- Now supports an openapi spec generated from a dotnet project (
Microsoft.OpenApi.Models
) - async client support fully working
- HTTP Bearer support
- HTTP Basic support
"},{"location":"CHANGELOG/#010","title":"0.1.0","text":" - Initial version
- Mostly works with a simple FastAPI generated spec (3.0.2)
- Works with Twilio's spec (see example_openapi_specs/ directory) (3.0.1)
- Almost works with stripes
"},{"location":"compatibility/","title":"\ud83d\udcb1 Compatibility","text":""},{"location":"compatibility/#great-compatibility","title":"Great compatibility","text":"Any standard 3.0.x
implementation works very well.
We have tested Clientele with:
- FastAPI - our target audience, so 100% compatibility guaranteed.
- drf-spectacular works great as well, you can see which schemas we tested in this GitHub issue.
- Microsoft's OpenAPI spec has also been battle tested and works well.
"},{"location":"compatibility/#no-compatibility","title":"No compatibility","text":"We do not support 2.x
aka \"Swagger\" - this format is quite different and deprecated.
"},{"location":"compatibility/#a-note-on-compatbility","title":"A note on compatbility","text":"When we were building Clientele, we discovered that, despite a fantastic specification, OpenAPI has a lot of poor implementations.
As pythonistas, we started with the auto-generated OpenAPI schemas provided by FastAPI, and then we branched out to large APIs like Twilio to test what we built.
Despite the effort, we still keep finding subtly different OpenAPI implementations.
Because of this we cannot guarentee 100% compatibility with an API, but we can give you a good indication of what we've tested.
"},{"location":"design/","title":"\ud83c\udfa8 Design","text":""},{"location":"design/#openapi-code-generators","title":"OpenAPI code generators","text":"Every few years we check the HTTP API landscape to see what has changed, and what new tooling is available. A part of this research is seeing how far OpenAPI client generators have come.
In the early years of OpenAPI, the \"last mile\" (i.e. - generating, and using, a client library) had a pretty poor experience:
-
The generated code was difficult to read, leading to problems when debugging the code. It was often not idiomatic to the language.
-
It was often bloated and repetitive, making changes tedious to support your specific project.
-
Also, you had to install a lot of things like Java to generate the code.
-
And finally, some API services still weren't publishing an OpenAPI schema. Or, the schema they published was different from the standard, so would not work with code generators.
This experience wasn't ideal, and was often such an impedance that it put us off using them. We would prefer instead to take a template of our own choosing, refined from years of working with HTTP APIs, and adapt it to whatever new API we were consuming.
"},{"location":"design/#the-landscape-in-2023","title":"The landscape in 2023","text":"In the early part of 2023, we had to build an integration with a new HTTP API. So, like we did in the past, we used it as an opportunity to asses the landscape of OpenAPI client generators.
And this was our summary at the time of writing:
-
API tools and providers had adopted OpenAPI very well. For example - tools like FastAPI and drf-spectacular now make it easy for the most popular python web frameworks to publish OpenAPI schemas.
-
There are a lot of options for generating clients. They all meet the need of \"generating a python client using the schema\". But, almost universally, they have a poor developer experience.
After evaluating many python client generators, we opted to use none of them and hand craft the API client ourselves. We used the OpenAPI schema as a source to develop the input and output objects. Then we wrote a small functional abstraction over the paths.
Looking back over our organised, pythonic, minimal client integration, we had an idea:
If this is the type of client we would like a generator to produce; how hard could it be to work backwards and build one?
This was the start of Clientele.
"},{"location":"design/#clientele","title":"Clientele","text":"As python developers ourselves, we know what makes good, readable, idiomatic python.
We also feel like we have a grasp on the best tools to be using in our python projects.
By starting with a client library that we like to use, and working backwards, we were able to build a client generator that produced something that python developers actually wanted.
But what is it exactly that we aimed to do with Clientele, why is this the OpenAPI Python client that you should use?
"},{"location":"design/#strongly-typed-inputs-and-outputs","title":"Strongly-typed inputs and outputs","text":"OpenAPI prides itself on being able to describe the input and output objects in it's schema.
This means you can build strongly-typed interfaces to the API. This helps to solve some common niggles when using an API - such as casting a value to a string when it should be an integer.
With Clientele, we opted to use Pydantic to build the models from the OpenAPI schema.
Pydantic doesn't only describe the shape of an object, it also validates the attributes as well. If an API sends back the wrong attributes at run time, Pydantic will error and provide a detail description about what went wrong.
"},{"location":"design/#idiomatic-python","title":"Idiomatic Python","text":"A lot of the client generators we tested produced a lot of poor code.
It was clear in a few cases that the client generator was built without understanding or knowledge of good python conventions.
In more than one case we also discovered the client generator would work by reading a file at run time. This is a very cool piece of engineering, but it is impractical to use. When you develop with these clients, the available functions and objects don't exist and you can't use an IDE's auto-complete feature.
Clientele set out to be as pythonic as possible. We use modern tools, idiomatic conventions, and provide some helpful bonuses like Black auto-formatting.
"},{"location":"design/#easy-to-understand","title":"Easy to understand","text":"Eventually a developer will need to do some debugging, and sometimes they'll need to do it in the generated client code.
A lot of the other client generators make obscure or obtuse code that is hard to pick apart and debug.
Now, there is a suggestion that developers shouldn't have to look at this, and that is fair. But the reality doesn't match that expectation. Personally; we like to know what generated code is doing. We want to trust that it will work, and that we can adapt around it if needs be. An interesting quirk of any code generator is you can't always inspect the source when evaluating to use it. Any other tool - you'd just go to GitHub and have a read around, but you can't with code generators.
So the code that is generated needs to be easy to understand.
Clientele doesn't do any tricks, or magic, or anything complex. The generated code has documentation and is designed to be readable. It is only a small layer on top of already well established tools, such as HTTPX.
In fact, we have some generated clients in our project repository so you can see what it looks like. We even have example tests for you to learn how to integrate with it.
It is that way because we know you will need to inspect it in the future. We want you to know that this is a sane and sensible tool.
"},{"location":"examples/","title":"\ud83e\ude84 Client example","text":"Let's build an API Client using clientele.
Our GitHub has a bunch of schemas that are proven to work with clientele, so let's use one of those!
"},{"location":"examples/#generate-the-client","title":"Generate the client","text":"In your project's root directory:
clientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o my_client/\n
Note
The example above uses one of our test schemas, and will work if you copy/paste it!
The -u
parameter expects a URL, you can provide a path to a file with -f
instead if you download the file.
The -o
parameter is the output directory of the generated client.
Run it now and you will see this output:
my_client/\n __init__.py\n client.py\n config.py\n http.py\n MANIFEST\n schemas.py\n
Let's go over each file and talk about what it does.
"},{"location":"examples/#client","title":"Client","text":""},{"location":"examples/#get-functions","title":"GET functions","text":"The client.py
file provides all the API functions from the OpenAPI schema. Functions are a combination of the path and the HTTP method for those paths. So, a path with two HTTP methods will be turned into two python functions.
my_client/client.pyfrom my_client import http, schemas\n\n\ndef simple_request_simple_request_get() -> schemas.SimpleResponse:\n \"\"\"Simple Request\"\"\"\n\n response = http.get(url=\"/simple-request\")\n return http.handle_response(simple_request_simple_request_get, response)\n\n...\n
We can see one of the functions here, simple_request_simple_request_get
, is for a straight-forward HTTP GET request without any input arguments, and it returns a schema object.
Here is how you might use it:
from my_client import client\n\nclient.simple_request_simple_request_get()\n>>> SimpleResponse(name='Hello, clientele')\n
"},{"location":"examples/#post-and-put-functions","title":"POST and PUT functions","text":"A more complex example is shown just below.
This is for an HTTP POST method, and it requires an input property called data
that is an instance of a schema, and returns one of many potential responses. If the endpoint has url parameters or query parameters, they will appear as input arguments to the function alongside the data
argument.
def request_data_request_data_post(\n data: schemas.RequestDataRequest\n) -> schemas.RequestDataResponse | schemas.HTTPValidationError:\n \"\"\"Request Data\"\"\"\n\n response = http.post(url=\"/request-data\", data=data.model_dump())\n return http.handle_response(request_data_request_data_post, response)\n
Here is how you might use it:
from my_client import client, schemas\n\ndata = schemas.RequestDataRequest(my_input=\"Hello, world\")\nresponse = client.request_data_request_data_post(data=data)\n>>> RequestDataResponse(your_input='Hello, world')\n
Clientele also supports the major HTTP methods PUT and DELETE in the same way.
"},{"location":"examples/#url-and-query-parameters","title":"URL and Query parameters","text":"If your endpoint takes path parameters (aka URL parameters) then clientele will turn them into parameters in the function:
from my_client import client\n\nclient.parameter_request_simple_request(your_input=\"gibberish\")\n>>> ParameterResponse(your_input='gibberish')\n
Query parameters will also be generated the same way. See this example for a function that takes a required query parameter.
Note that, optional parameters that are not passed will be omitted when the URL is generated by Clientele.
"},{"location":"examples/#handling-responses","title":"Handling responses","text":"Because we're using Pydantic to manage the input data, we get a strongly-typed response object. This works beautifully with the new structural pattern matching feature in Python 3.10 and up:
response = client.request_data_request_data_post(data=data)\n\n# Handle responses elegantly\nmatch response:\n case schemas.RequestDataResponse():\n # Handle valid response\n ...\n case schemas.ValidationError():\n # Handle validation error\n ...\n
"},{"location":"examples/#api-exceptions","title":"API Exceptions","text":"Clientele keeps a mapping of the paths and their potential response codes. When it gets a response code that fits into the map, it generates the pydantic object associated to it.
If the HTTP response code is an unintended one, it will not match a return type. In this case, the function will raise an http.APIException
.
from my_client import client, http\ntry:\n good_response = my_client.get_my_thing()\nexcept http.APIException as e:\n # The API got a response code we didn't expect\n print(e.response.status_code)\n
The response
object will be attached to this exception class for your own debugging.
"},{"location":"examples/#schemas","title":"Schemas","text":"The schemas.py
file has all the possible schemas, request and response, and even Enums, for the API. These are taken from OpenAPI's schemas objects and turned into Python classes. They are all subclassed from pydantic's BaseModel
.
Here are a few examples:
my_client/schemas.pyimport pydantic\nfrom enum import Enum\n\n\nclass ParameterResponse(pydantic.BaseModel):\n your_input: str\n\nclass RequestDataRequest(pydantic.BaseModel):\n my_input: str\n\nclass RequestDataResponse(pydantic.BaseModel):\n my_input: str\n\n# Enums subclass str so they serialize to JSON nicely\nclass ExampleEnum(str, Enum):\n ONE = \"ONE\"\n TWO = \"TWO\"\n
"},{"location":"examples/#configuration","title":"Configuration","text":"One of the problems with auto-generated clients is that you often need to configure them, and if you try and regenerate the client at some point in the future then your configuration gets wiped clean and you have to do it all over again.
Clientele solves this problem by providing an entry point for configuration that will never be overwritten - config.py
.
When you first generate the project, you will see a file called config.py
and it will offer configuration functions a bit like this:
\"\"\"\nThis file will never be updated on subsequent clientele runs.\nUse it as a space to store configuration and constants.\n\nDO NOT CHANGE THE FUNCTION NAMES\n\"\"\"\n\n\ndef api_base_url() -> str:\n \"\"\"\n Modify this function to provide the current api_base_url.\n \"\"\"\n return \"http://localhost\"\n
Subsequent runs of the generate
command with --regen t
will not change this file if it exists, so you are free to modify the defaults to suit your needs.
For example, if you need to source the base url of your API for different configurations, you can modify the api_base_url
function like this:
from my_project import my_config\n\ndef api_base_url() -> str:\n \"\"\"\n Modify this function to provide the current api_base_url.\n \"\"\"\n if my_config.debug:\n return \"http://localhost:8000\"\n elif my_config.production:\n return \"http://my-production-url.com\"\n
Just keep the function names the same and you're good to go.
"},{"location":"examples/#authentication","title":"Authentication","text":"If your OpenAPI spec provides security information for the following authentication methods:
- HTTP Bearer
- HTTP Basic
Then clientele will provide you information on the environment variables you need to set to make this work during the generation. For example:
Please see my_client/config.py to set authentication variables\n
The config.py
file will have entry points for you to configure, for example, HTTP Bearer authentication will need the get_bearer_token
function to be updated, something like this:
def get_bearer_token() -> str:\n \"\"\"\n HTTP Bearer authentication.\n Used by many authentication methods - token, jwt, etc.\n Does not require the \"Bearer\" content, just the key as a string.\n \"\"\"\n from os import environ\n return environ.get(\"MY_AUTHENTICATION_TOKEN\")\n
"},{"location":"examples/#additional-headers","title":"Additional headers","text":"If you want to pass specific headers with all requests made by the client, you can configure the additional_headers
function in config.py
to do this.
def additional_headers() -> dict:\n \"\"\"\n Modify this function ot provide additional headers to all\n HTTP requests made by this client.\n \"\"\"\n return {}\n
Please note that if you are using this with authentication headers, then authentication headers will overwrite these defaults if they keys match.
"},{"location":"install/","title":"\ud83c\udfd7\ufe0f Install","text":"We recommend installing with pipx as a global CLI command:
pipx install clientele\n
Once installed you can run clientele version
to make sure you have the latest version:
> clientele version\nclientele 0.7.0\n
"},{"location":"testing/","title":"Testing","text":"Clientele is designed for easy testing, and our own test suite is a great example of how easily you can write mock tests for your API Client.
import pytest\nfrom httpx import Response\nfrom respx import MockRouter\n\nfrom .test_client import client, constants, schemas\n\nBASE_URL = constants.api_base_url()\n\n\n@pytest.mark.respx(base_url=BASE_URL)\ndef test_simple_request_simple_request_get(respx_mock: MockRouter):\n # Given\n mocked_response = {\"status\": \"hello world\"}\n mock_path = \"/simple-request\"\n respx_mock.get(mock_path).mock(\n return_value=Response(json=mocked_response, status_code=200)\n )\n # When\n response = client.simple_request_simple_request_get()\n # Then\n assert isinstance(response, schemas.SimpleResponse)\n assert len(respx_mock.calls) == 1\n call = respx_mock.calls[0]\n assert call.request.url == BASE_URL + mock_path\n
We recommend you install respx for writing your tests.
"},{"location":"usage/","title":"\ud83d\udcdd Use Clientele","text":"Note
You can type clientele COMMAND --help
at anytime to see explicit information about the available arguments.
"},{"location":"usage/#generate","title":"generate
","text":"Generate a Python HTTP Client from an OpenAPI Schema.
"},{"location":"usage/#from-a-url","title":"From a URL","text":"Use the -u
or --url
argument.
-o
or --output
is the target directory for the generate client.
clientele generate -u https://raw.githubusercontent.com/phalt/clientele/main/example_openapi_specs/best.json -o my_client/\n
Note
The example above uses one of our test schemas, and will work if you copy/paste it!
"},{"location":"usage/#from-a-file","title":"From a file","text":"Alternatively you can provide a local file using the -f
or --file
argument.
clientele generate -f path/to/file.json -o my_client/\n
"},{"location":"usage/#asyncio","title":"Async.io","text":"If you prefer an asyncio client, just pass --asyncio t
to your command.
clientele generate -f path/to/file.json -o my_client/ --asyncio t\n
"},{"location":"usage/#regenerating","title":"Regenerating","text":"At times you may wish to regenerate the client. This could be because the API has updated or you just want to use a newer version of clientele.
To force a regeneration you must pass the --regen
or -r
argument, for example:
clientele generate -f example_openapi_specs/best.json -o my_client/ --regen t\n
Note
You can copy and paste the command from the MANIFEST.md
file in your previously-generated client for a quick and easy regeneration.
"},{"location":"usage/#validate","title":"validate
","text":"Validate lets you check if an OpenAPI schema will work with clientele.
Note
Some OpenAPI schema generators do not conform to the specification.
Clientele uses openapi-core to validate the schema.
"},{"location":"usage/#from-a-url_1","title":"From a URL","text":"Use the -u
or --url
argument.
-o
or --output
is the target directory for the generate client.
clientele validate -u http://path.com/to/openapi.json\n
"},{"location":"usage/#from-a-file-path","title":"From a file path","text":"Alternatively you can provide a local file using the -f
or --file
argument.
clientele validate -f /path/to/openapi.json\n
"},{"location":"usage/#generate-basic","title":"generate-basic","text":"The generate-basic
command can be used to generate a basic file structure for an HTTP client.
It does not required an OpenAPI schema, just a path.
This command serves two reasons:
- You may have an HTTP API without an OpenAPI schema and you want to keep a consistent file structure with other Clientele clients.
- The generator for this basic client can be extended for your own client in the future, if you choose.
clientele generate-basic -o my_client/\n
"},{"location":"usage/#version","title":"version
","text":"Print the current version of Clientele:
> clientele version\nClientele 0.7.0\n
"}]}
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
index 9fdcefb..08d4fc3 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,42 +2,42 @@
https://phalt.github.io/clientele/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/CHANGELOG/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/compatibility/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/design/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/examples/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/install/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/testing/
- 2023-10-08
+ 2023-10-24
daily
https://phalt.github.io/clientele/usage/
- 2023-10-08
+ 2023-10-24
daily
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index b5972e0..b00e65a 100644
Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ
diff --git a/usage/index.html b/usage/index.html
index 9f0501e..0f78fd2 100644
--- a/usage/index.html
+++ b/usage/index.html
@@ -334,6 +334,13 @@
Async.io
+
+
+ -
+
+ Regenerating
+
+
@@ -366,6 +373,13 @@
+
+
+ -
+
+ generate-basic
+
+
-
@@ -531,6 +545,13 @@
Async.io
+
+
+ -
+
+ Regenerating
+
+
@@ -563,6 +584,13 @@
+
+
+ -
+
+ generate-basic
+
+
-
@@ -614,6 +642,15 @@
Async.io
If you prefer an asyncio client, just pass --asyncio t
to your command.
+Regenerating
+At times you may wish to regenerate the client. This could be because the API has updated or you just want to use a newer version of clientele.
+To force a regeneration you must pass the --regen
or -r
argument, for example:
+
+
+Note
+You can copy and paste the command from the MANIFEST.md
file in your previously-generated client for a quick and easy regeneration.
+
validate
Validate lets you check if an OpenAPI schema will work with clientele.
@@ -624,16 +661,26 @@ validate
From a URL
Use the -u
or --url
argument.
-o
or --output
is the target directory for the generate client.
-clientele validate -u http://path.com/to/openapi.json
+
From a file path
Alternatively you can provide a local file using the -f
or --file
argument.
-clientele validate -f /path/to/openapi.json
+
+generate-basic
+The generate-basic
command can be used to generate a basic file structure for an HTTP client.
+It does not required an OpenAPI schema, just a path.
+This command serves two reasons:
+
+- You may have an HTTP API without an OpenAPI schema and you want to keep a consistent file structure with other Clientele clients.
+- The generator for this basic client can be extended for your own client in the future, if you choose.
+
+
version
Print the current version of Clientele:
-