Skip to content

Create fully typed declarative API clients quickly and easily.

License

Notifications You must be signed in to change notification settings

martinn/quickapiclient

Repository files navigation

Quick Api Client

Release Build status codecov

A library for creating fully typed declarative API clients quickly and easily.

A basic example

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]


# We define an API endpoint
class GetFactsApi(quickapi.BaseApi[ResponseBody]):
    url = "/facts"
    response_body = ResponseBody


# And now our API client
class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    get_facts = quickapi.ApiEndpoint(GetFactsApi)
    # Other endpoints would follow here:
    # submit_fact = quickapi.ApiEndpoint(SubmitFactApi)

And you would use it like this:

client = ExampleClient()
response = client.get_facts()

# `response` is fully typed and conforms to our `ResponseBody` definition
assert isinstance(response.body, ResponseBody)
assert isinstance(response.body.data[0], Fact)

# `reveal_type(response.body)` returns `Revealed type is 'ResponseBody'` too,
# which means full typing and IDE support.

There's also support for attrs, pydantic and msgspec for more complex modeling, validation or serialization support.

Scroll down here for examples using those.

Features

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
    • Fully typed HTTP status error codes handling
    • Nested/inner class definitions
    • Generate API boilerplate from OpenAPI specs
  • HTTP client libraries
    • httpx
    • requests
    • aiohttp
  • Authentication mechanisms
    • Basic Auth
    • Token / JWT
    • Digest
    • NetRC
    • Any auth supported by httpx or httpx_auth or requests, including custom schemes
  • Serialization/deserialization
    • attrs
    • dataclasses
    • pydantic
    • msgspec
  • API support
    • REST
    • GraphQL
    • Others?
  • Response types supported
    • JSON
    • XML
    • Others?

Installation

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 dataclasses:
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 dataclasses:
poetry add quickapiclient[msgspec]
# Or if you want to use requests over httpx:
poetry add quickapiclient[requests]

More examples

A GET request with query params

An example of a GET request with query parameters with overridable default values.

Click to expand
# ...

@dataclass
class RequestParams:
    max_length: int = 100
    limit: int = 10


class GetFactsApi(quickapi.BaseApi[ResponseBody]):
    url = "/facts"
    request_params = RequestParams
    response_body = ResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    get_facts = quickapi.ApiEndpoint(GetFactsApi)

And to use it:

client = ExampleClient()
# Using default request param values
response = client.get_facts()

# Using custom request param values
request_params = RequestParams(max_length=5, limit=10)
response = client.get_facts(request_params=request_params)

A POST request

An example of a POST request with some optional and required data.

Click to expand
# ...

@dataclass
class RequestBody:
    required_input: str
    optional_input: str | None = None


@dataclass
class SubmitResponseBody:
    success: bool
    message: str


class SubmitFactApi(quickapi.BaseApi[SubmitResponseBody]):
    url = "/facts/new"
    method = quickapi.BaseHttpMethod.POST
    request_body = RequestBody
    response_body = SubmitResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    get_facts = quickapi.ApiEndpoint(GetFactsApi)
    submit_fact = quickapi.ApiEndpoint(SubmitFactApi)

And to use it:

client = ExampleClient()
request_body = RequestBody(required_input="dummy")
response = client.submit_fact(request_body=request_body)

A POST request with authentication

An example of a POST request with HTTP header API key.

Click to expand
import httpx_auth

# ...

class SubmitFactApi(quickapi.BaseApi[SubmitResponseBody]):
    url = "/facts/new"
    method = quickapi.BaseHttpMethod.POST
    # Specify it here if you want all requests to this endpoint to have auth
    # auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
    request_body = RequestBody
    response_body = SubmitResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    # Specify it here if you want requests to all of this clients' endpoints to have auth
    # auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
    get_facts = quickapi.ApiEndpoint(GetFactsApi)
    submit_fact = quickapi.ApiEndpoint(SubmitFactApi)

And to use it:

auth = httpx_auth.HeaderApiKey(header_name="X-Api-Key", api_key="secret_api_key")
client = ExampleClient(
  # Specify it here to have auth for all apis on this client instance only
  # auth=auth
)
request_body = RequestBody(required_input="dummy")
response = client.submit_fact(
  request_body=request_body,
  # Or here to have auth just for this api request
  auth=auth
)

A POST request with error handling

An example of a POST request that handles HTTP error codes too.

Click to expand
# ...

@dataclass
class ResponseError401:
    status: str
    message: str


class SubmitFactApi(quickapi.BaseApi[SubmitResponseBody]):
    url = "/facts/new"
    method = quickapi.BaseHttpMethod.POST
    response_body = ResponseBody
    # Add more types for each HTTP status code you wish to handle
    response_errors = {401: ResponseError401}


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    submit_fact = quickapi.ApiEndpoint(SubmitFactApi)

And to use it:

client = ExampleClient()
request_body = RequestBody(required_input="dummy")

try:
    response = client.submit_fact(request_body=request_body)
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.")

A POST request with validation and conversion (Using attrs)

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 SubmitApi(quickapi.BaseApi[ResponseBody]):
    url = "/submit"
    method = quickapi.BaseHttpMethod.POST
    request_body = RequestBody
    response_body = ResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    submit = quickapi.ApiEndpoint(SubmitApi)

And to use it:

client = ExampleClient()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.submit(request_body=request_body)

Check out attrs for full configuration.

A POST request with validation and conversion (Using pydantic)

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 SubmitApi(quickapi.BaseApi[ResponseBody]):
    url = "/submit"
    method = quickapi.BaseHttpMethod.POST
    request_body = RequestBody
    response_body = ResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    submit = quickapi.ApiEndpoint(SubmitApi)

And to use it:

client = ExampleClient()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.submit(request_body=request_body)

Check out pydantic for full configuration.

A POST request with validation and conversion (Using msgspec)

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 SubmitApi(quickapi.BaseApi[ResponseBody]):
    url = "/submit"
    method = quickapi.BaseHttpMethod.POST
    request_body = RequestBody
    response_body = ResponseBody


class ExampleClient(quickapi.BaseClient):
    base_url = "https://example.com"
    submit = quickapi.ApiEndpoint(SubmitApi)

And to use it:

client = ExampleClient()
request_body = RequestBody(email="invalid_email", state="on") # Will raise an error
response = client.submit(request_body=request_body)

Using requests library

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 GetFactsApi(quickapi.BaseApi[ResponseBody]):
    url = "/facts"
    response_body = ResponseBody


class ExampleClient(quickapi.BaseClient)
    base_url = "https://example.com"
    http_client = quickapi.RequestsClient()
    get_facts = quickapi.ApiEndpoint(GetFactsApi)

And to use it:

client = ExampleClient()
response = client.get_facts()

Goal

Eventually, I would like for the API client definition to end up looking more like this:

Click to expand
import quickapi


@quickapi.api
class FetchApi:
    url = "/fetch"
    method = quickapi.BaseHttpMethod.GET

    class ResponseBody:
        current_page: int
        data: list[Fact]


@quickapi.api
class SubmitApi:
    url = "/submit"
    method = quickapi.BaseHttpMethod.POST

    class RequestBody:
        required_input: str
        optional_input: str | None = None

    class ResponseBody:
        success: bool
        message: str


@quickapi.client
class MyClient:
    base_url = "https://example.com"
    fetch = quickapi.ApiEndpoint(FetchApi)
    submit = quickapi.ApiEndpoint(SubmitApi)


client = MyClient(auth=...)
response = client.fetch()
response = client.submit(request_body=RequestBody(...))

Contributing

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.