Building an API Client¶
This page will walk you through steps on how to build wrapper for API.
Making a Request¶
Each wrapper needs a HTTP client to make requests to the API.
You can easily use one of the drivers to make requests, but
Driver.fetch()
call returns a
Response
object, which is not always
suitable for building good API clients.
For API client it can be better to return typed data, such as dataclasses, than let the final user deal with json.
apiwrappers provides a fetch()
function,
which takes driver as a first argument, and all other
arguments are the same as with
Driver.fetch()
.
Giving that, it behaves exactly like if you are working with driver:
>>> from apiwrappers import Request, fetch, make_driver
>>> driver = make_driver("requests")
>>> request = Request("GET", "https://example.org")
>>> response = fetch(driver, request)
<Response [200]>
You can also provide two additional arguments:
model
- a type or factory function that describes response structure.source
- optional key name in the json, which value will be passed to themodel
. You may use dotted notation to traverse keys -key1.key2
With these arguments, fetch()
function acts like
a factory, returning new instance of the type provided to the model
argument:
from dataclasses import dataclass
from typing import List
from apiwrappers import Request, fetch, make_driver
@dataclass
class Repo:
name: str
url = "https://api.github.com/users/unmade/repos"
request = Request("GET", url)
driver = make_driver("requests")
fetch(driver, request, model=List[Repo]) # [Repo(name='am-date-picker'), ...]
fetch(driver, request, model=Repo, source="0") # Repo(name='am-date-picker')
fetch(driver, request, model=str, source="0.name") # 'am-date-picker'
driver = make_driver("aiohttp")
await fetch(driver, request, model=List[Repo]) # [Repo(name='am-date-picker'), ...]
await fetch(driver, request, model=Repo, source="0") # Repo(name='am-date-picker')
await fetch(driver, request, model=str, source="0.name") # 'am-date-picker'
Writing a Simple API Client¶
Now that we know how to make requests and how to get data from response, lets write API client class:
from dataclasses import dataclass
from typing import List
from apiwrappers import Request, Url, fetch
@dataclass
class Repo:
id: int
name: str
class GitHub:
def __init__(self, host, driver):
self.url = Url(host)
self.driver = driver
def get_repos(self, username):
url = self.url("/users/{username}/repos", username=username)
request = Request("GET", url)
return fetch(self.driver, request, model=List[Repo])
Here we defined .get_repos
method to get all user’s repos.
Based on the driver this method returns either a List[Repo]
or a coroutine - Awaitable[List[Repo]]
You never want to await the fetch call here, just return it immediately and let the final user await it if needed
Another thing to notice is how we create URL:
url = self.url("/users/{username}/repos", username=username)
Sometimes, it’s useful to have an URL template, for example, for logging or for aggregating metrics, so instead of formatting immediately, we provide a template and replacement fields.
The wrapper above is good enough to satisfy most cases, however it lacks one of the important features nowadays - type annotations.
Adding Type Annotations¶
In the example above, we didn’t add any type annotations for
.get_repos
method.
We can simply specify return type as:
Union[List[Repo], Awaitable[List[Repo]]
and that will be enough to have a good auto-completion, but what we want precise type annotations.
We want to tell mypy, that when driver corresponds to
Driver
protocol
.get_repos
has return type List[Repo]
and for AsyncDriver
protocol -
Awaitable[List[Repo]]
.
It can be done like that:
from __future__ import annotations
from dataclasses import dataclass
from typing import Awaitable, Generic, List, TypeVar, overload
from apiwrappers import AsyncDriver, Driver, Request, Url, fetch
T = TypeVar("T", Driver, AsyncDriver)
@dataclass
class Repo:
id: int
name: str
class GitHub(Generic[T]):
def __init__(self, host: str, driver: T):
self.url = Url(host)
self.driver: T = driver
@overload
def get_repos(
self: GitHub[Driver], username: str
) -> List[Repo]:
...
@overload
def get_repos(
self: GitHub[AsyncDriver], username: str
) -> Awaitable[List[Repo]]:
...
def get_repos(self, username: str):
url = self.url("/users/{username}/repos", username=username)
request = Request("GET", url)
return fetch(self.driver, request, model=List[Repo])
Here, we defined a T
type variable, constrained to
Driver
and AsyncDriver
protocols.
Our wrapper is now a generic class of that variable.
We also used overload
with self-type to define return type based on
the driver provided to our wrapper.
Using the API Client¶
Here is how we can use our client:
>>> from apiwrappers import make_driver
>>> driver = make_driver("requests")
>>> github = GitHub("https://api.github.com", driver=driver)
>>> github.get_repos("unmade")
[Repo(id=47463599, name='am-date-picker'),
...
]
Or to use it asynchronously:
>>> from apiwrappers import make_driver
>>> driver = make_driver("aiohttp")
>>> github = GitHub("https://api.github.com", driver=driver)
>>> await github.get_repos("unmade")
[Repo(id=47463599, name='am-date-picker'),
...
]