A library for creating fully typed declarative API clients quickly and easily.
- Github repository: https://github.com/martinn/quickapiclient/
- Documentation https://martinn.github.io/quickapiclient/
An API definition for a simple service could look like this:
from dataclasses import dataclass
import quickapi
# An example type that will be part of the API response
@dataclass
class Fact:
fact: str
length: int
# What the API response should look like
@dataclass
class ResponseBody:
current_page: int
data: list[Fact]
# Now we can define our API
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://catfact.ninja/facts"
response_body = ResponseBody
And you would use it like this:
api_client = MyApi()
response = api_client.execute()
# That's it! Now `response` is fully typed and conforms to our `ResponseBody` definition
assert isinstance(response.body, ResponseBody)
assert isinstance(response.body.data[0], Fact)
There's also support for attrs
or pydantic
for more complex modeling, validation or serialization support.
Scroll down here for examples using those.
It's still early development but so far we have support for:
- Write fully typed declarative API clients quickly and easily
- Fully typed request params / body
- Fully typed response body
- Serialization/deserialization support
- Basic error and serialization handling
- Nested/inner class definitions
- Fully typed HTTP status error codes handling
- Sessions support and/or allow building several related APIs through a single interface
- Generate API boilerplate from OpenAPI specs
- Full async support
- HTTP client libraries
- httpx
- requests
- aiohttp
- Authentication mechanisms
- Basic Auth
- Token / JWT
- Digest
- NetRC
- Any auth supported by
httpx
or httpx_auth orrequests
, including custom schemes
- Serialization/deserialization
- attrs
- dataclasses
- pydantic
- msgspec
- API support
- REST
- GraphQL
- Others?
- Response types supported
- JSON
- XML
- Others?
You can easily install this using pip
:
pip install quickapiclient
# Or if you want to use `attrs` over `dataclasses`:
pip install quickapiclient[attrs]
# Or if you want to use `pydantic` over `dataclasses`:
pip install quickapiclient[pydantic]
# Or if you want to use `msgspec` over `pydantic`:
pip install quickapiclient[msgspec]
# Or if you want to use `requests` over `httpx`:
pip install quickapiclient[requests]
Or if using poetry
:
poetry add quickapiclient
# Or if you want to use `attrs` over `dataclasses`:
poetry add quickapiclient[attrs]
# Or if you want to use `pydantic` over `dataclasses`:
poetry add quickapiclient[pydantic]
# Or if you want to use `msgspec` over `pydantic`:
poetry add quickapiclient[msgspec]
# Or if you want to use `requests` over `httpx`:
poetry add quickapiclient[requests]
An example of a GET request with query parameters with overridable default values.
Click to expand
from dataclasses import dataclass
import quickapi
@dataclass
class RequestParams:
max_length: int = 100
limit: int = 10
@dataclass
class Fact:
fact: str
length: int
@dataclass
class ResponseBody:
current_page: int
data: list[Fact]
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://catfact.ninja/facts"
request_params = RequestParams
response_body = ResponseBody
And to use it:
client = MyApi()
# Using default request param values
response = client.execute()
# Using custom request param values
request_params = RequestParams(max_length=5, limit=10)
response = client.execute(request_params=request_params)
An example of a POST request with some optional and required data.
Click to expand
from dataclasses import dataclass
import quickapi
@dataclass
class RequestBody:
required_input: str
optional_input: str | None = None
@dataclass
class Fact:
fact: str
length: int
@dataclass
class ResponseBody:
current_page: int
data: list[Fact]
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://catfact.ninja/facts"
method = quickapi.BaseHttpMethod.POST
request_body = RequestBody
response_body = ResponseBody
And to use it:
client = MyApi()
request_body = RequestBody(required_input="dummy")
response = client.execute(request_body=request_body)
An example of a POST request with HTTP header API key.
Click to expand
from dataclasses import dataclass
import httpx_auth
import quickapi
@dataclass
class RequestBody:
required_input: str
optional_input: str | None = None
@dataclass
class Fact:
fact: str
length: int
@dataclass
class AuthResponseBody:
authenticated: bool
user: str
class MyApi(quickapi.BaseApi[AuthResponseBody]):
url = "https://httpbin.org/bearer"
method = quickapi.BaseHttpMethod.POST
# You could specify it here if you wanted
# auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
response_body = AuthResponseBody
And to use it:
client = MyApi()
request_body = RequestBody(required_input="dummy")
auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
response = client.execute(request_body=request_body, auth=auth)
An example of a POST request with HTTP header API key that handles HTTP error codes too.
Click to expand
from dataclasses import dataclass
import httpx_auth
import quickapi
@dataclass
class RequestBody:
required_input: str
optional_input: str | None = None
@dataclass
class Fact:
fact: str
length: int
@dataclass
class AuthResponseBody:
authenticated: bool
user: str
@dataclass
class ResponseError401:
status: str
message: str
class MyApi(quickapi.BaseApi[AuthResponseBody]):
url = "https://httpbin.org/bearer"
method = quickapi.BaseHttpMethod.POST
response_body = AuthResponseBody
# Add more types for each HTTP status code you wish to handle
response_errors = {401: ResponseError401}
And to use it:
client = MyApi()
request_body = RequestBody(required_input="dummy")
auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="incorrect_api_key")
try:
response = client.execute(request_body=request_body, auth=auth)
except quickapi.HTTPError as e:
match e.value.status_code:
case 401:
assert isinstance(e.value.body, ResponseError401)
print(f"Received {e.value.body.status} with {e.value.body.message}")
case _:
print("Unhandled error occured.")
An example of a POST request with custom validators and converters (using attrs
instead).
Click to expand
import attrs
import quickapi
import enum
class State(enum.Enum):
ON = "on"
OFF = "off"
@attrs.define
class RequestBody:
state: State = attrs.field(validator=attrs.validators.in_(State))
email: str = attrs.field(
validator=attrs.validators.matches_re(
r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
)
)
@attrs.define
class ResponseBody:
success: bool = attrs.field(converter=attrs.converters.to_bool)
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://example.com/"
method = quickapi.BaseHttpMethod.POST
request_body = RequestBody
response_body = ResponseBody
And to use it:
client = MyApi()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.execute(request_body=request_body)
Check out attrs for full configuration.
An example of a POST request with custom validators and converters (using pydantic
instead).
Click to expand
import enum
import pydantic
import quickapi
class State(enum.Enum):
ON = "on"
OFF = "off"
class RequestBody(pydantic.BaseModel):
state: State
email: pydantic.EmailStr
class ResponseBody(pydantic.BaseModel):
success: bool
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://example.com/"
method = quickapi.BaseHttpMethod.POST
request_body = RequestBody
response_body = ResponseBody
And to use it:
client = MyApi()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.execute(request_body=request_body)
Check out pydantic for full configuration.
An example of a POST request with custom validators and converters (using msgspec
instead).
Click to expand
import enum
from typing import Annotated
import msgspec
import quickapi
class State(enum.Enum):
ON = "on"
OFF = "off"
class RequestBody(msgspec.Struct):
state: State
email: str = Annotated[str, msgspec.Meta(pattern=r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')]
class ResponseBody(msgspec.Struct):
success: bool
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://example.com/"
method = quickapi.BaseHttpMethod.POST
request_body = RequestBody
response_body = ResponseBody
And to use it:
client = MyApi()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.execute(request_body=request_body)
An example of a GET request using the requests
HTTP library instead of HTTPx
.
Click to expand
from dataclasses import dataclass
import quickapi
@dataclass
class ResponseBody:
current_page: int
data: list[Fact]
class MyApi(quickapi.BaseApi[ResponseBody]):
url = "https://catfact.ninja/facts"
response_body = ResponseBody
http_client = quickapi.RequestsClient()
And to use it:
client = MyApi()
response = client.execute()
You can easily create a client to manage related endpoints, and even share things like auth. This is done through pure Python at this stage, though we aim to make this a lot easier and streamlined in the future.
Click to expand
... [Assuming GetApi and SubmitApi have been already defined]
class ExampleClient:
fetch = GetApi
submit = SubmitApi
And to use it:
client = ExampleClient()
auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
http_client = httpx.Client()
# Calling the GetApi endpoint
response = client.fetch(auth=auth, http_client=http_client).execute()
# Calling the SubmitApi endpoint
response = client.submit(auth=auth, http_client=http_client).execute()
Eventually, I would like for the API definition to end up looking more like this (though the current approach will still be supported):
Click to expand
import quickapi
@quickapi.define
class SubmitApi:
url = "/submit"
method = quickapi.BaseHttpMethod.POST
class RequestBody:
required_input: str
optional_input: str | None = None
class ResponseBody:
current_page: int
data: list[Fact]
And if you had multiple related endpoints that could share HTTP session or auth:
@quickapi.define
class FetchApi:
url = "/fetch"
method = quickapi.BaseHttpMethod.GET
class ResponseBody:
current_page: int
data: list[Fact]
@quickapi.define_client
class MyClient:
base_url = "https://catfact.ninja"
fetch = FetchApi
submit = SubmitApi
client = MyClient(auth=...)
response = client.fetch()
response = client.submit(RequestBody(...))
Contributions are welcomed, and greatly appreciated!
The easiest way to contribute, if you found this useful or interesting, is by giving it a star! 🌟
Otherwise, check out the contributing guide for how else to help and get started.