Creating API clients that are âasync agnosticâ

Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
7
down vote
favorite
Python3's async/await syntax is great, but it does create a divide between libraries which are async-based and those which are not. For example, boto3 (AWS API library) currently doesn't work with async. There is a separate project, aiobotocore, attempts to recreate some of this functionality in an async context.
I have been thinking for a while about how to create HTTP API clients which can be run async and sync contexts. I have come up with a strategy that involves creating a separation between the logic (preparation of requests, interpretation of responses), and the sending. The logic is implemented as a generator function, which yields out an object which represents the request, and receives back an object representing the response. This generator is "run" by a runner function, which does the actual sending, and may be sync or async.
You can imagine that if, for example, boto3 had been written this way, it would be easy to re-use the bulk of the code to make an async version, rather than having to do an async rewrite.
I have included a toy example below. I would welcome comments.
from typing import Iterable, NamedTuple, Dict, Any, Optional
import requests
from aiohttp import ClientSession
URL_TEMPLATE = "https://api.icndb.com/jokes/id/"
class Request(NamedTuple):
method: str
url: str
json: Optional[Dict[str, Any]] = None
class Response(NamedTuple):
status: int
json: Optional[Dict[str, Any]] = None
def get_joke(id: int) -> Iterable[Request]:
response = yield Request("GET", URL_TEMPLATE.format(id=id))
try:
if response.status != 200:
raise JokeApiError("API request failed")
data = response.json
if data.get("type") == 'NoSuchQuoteException':
raise NoSuchJoke(data.get("value", ""))
if data.get("type") != "success":
raise JokeApiError("API request failed")
raise Return(data["value"]["joke"])
except (JokeApiError, Return, StopIteration):
raise
except Exception as e:
raise JokeApiError() from e
def call_api_sync(it):
try:
for req in it:
response = requests.request(req.method, req.url, json=req.json)
it.send(
Response(status=response.status_code, json=response.json())
)
except Return as e:
return e.value
async def call_api_async(it):
async with ClientSession() as session:
try:
for req in it:
async with session.request(
method=req.method,
url=req.url,
json=req.json) as res:
json = await res.json()
response = Response(status=res.status, json=json)
it.send(response)
except Return as e:
return e.value
class JokeApiError(Exception):
pass
class NoSuchJoke(JokeApiError):
pass
class Return(Exception):
def __init__(self, value):
self.value = value
python python-3.x async-await
add a comment |Â
up vote
7
down vote
favorite
Python3's async/await syntax is great, but it does create a divide between libraries which are async-based and those which are not. For example, boto3 (AWS API library) currently doesn't work with async. There is a separate project, aiobotocore, attempts to recreate some of this functionality in an async context.
I have been thinking for a while about how to create HTTP API clients which can be run async and sync contexts. I have come up with a strategy that involves creating a separation between the logic (preparation of requests, interpretation of responses), and the sending. The logic is implemented as a generator function, which yields out an object which represents the request, and receives back an object representing the response. This generator is "run" by a runner function, which does the actual sending, and may be sync or async.
You can imagine that if, for example, boto3 had been written this way, it would be easy to re-use the bulk of the code to make an async version, rather than having to do an async rewrite.
I have included a toy example below. I would welcome comments.
from typing import Iterable, NamedTuple, Dict, Any, Optional
import requests
from aiohttp import ClientSession
URL_TEMPLATE = "https://api.icndb.com/jokes/id/"
class Request(NamedTuple):
method: str
url: str
json: Optional[Dict[str, Any]] = None
class Response(NamedTuple):
status: int
json: Optional[Dict[str, Any]] = None
def get_joke(id: int) -> Iterable[Request]:
response = yield Request("GET", URL_TEMPLATE.format(id=id))
try:
if response.status != 200:
raise JokeApiError("API request failed")
data = response.json
if data.get("type") == 'NoSuchQuoteException':
raise NoSuchJoke(data.get("value", ""))
if data.get("type") != "success":
raise JokeApiError("API request failed")
raise Return(data["value"]["joke"])
except (JokeApiError, Return, StopIteration):
raise
except Exception as e:
raise JokeApiError() from e
def call_api_sync(it):
try:
for req in it:
response = requests.request(req.method, req.url, json=req.json)
it.send(
Response(status=response.status_code, json=response.json())
)
except Return as e:
return e.value
async def call_api_async(it):
async with ClientSession() as session:
try:
for req in it:
async with session.request(
method=req.method,
url=req.url,
json=req.json) as res:
json = await res.json()
response = Response(status=res.status, json=json)
it.send(response)
except Return as e:
return e.value
class JokeApiError(Exception):
pass
class NoSuchJoke(JokeApiError):
pass
class Return(Exception):
def __init__(self, value):
self.value = value
python python-3.x async-await
I'd suggest adding aif name...section as well
â hjpotter92
Jun 28 at 17:23
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13
add a comment |Â
up vote
7
down vote
favorite
up vote
7
down vote
favorite
Python3's async/await syntax is great, but it does create a divide between libraries which are async-based and those which are not. For example, boto3 (AWS API library) currently doesn't work with async. There is a separate project, aiobotocore, attempts to recreate some of this functionality in an async context.
I have been thinking for a while about how to create HTTP API clients which can be run async and sync contexts. I have come up with a strategy that involves creating a separation between the logic (preparation of requests, interpretation of responses), and the sending. The logic is implemented as a generator function, which yields out an object which represents the request, and receives back an object representing the response. This generator is "run" by a runner function, which does the actual sending, and may be sync or async.
You can imagine that if, for example, boto3 had been written this way, it would be easy to re-use the bulk of the code to make an async version, rather than having to do an async rewrite.
I have included a toy example below. I would welcome comments.
from typing import Iterable, NamedTuple, Dict, Any, Optional
import requests
from aiohttp import ClientSession
URL_TEMPLATE = "https://api.icndb.com/jokes/id/"
class Request(NamedTuple):
method: str
url: str
json: Optional[Dict[str, Any]] = None
class Response(NamedTuple):
status: int
json: Optional[Dict[str, Any]] = None
def get_joke(id: int) -> Iterable[Request]:
response = yield Request("GET", URL_TEMPLATE.format(id=id))
try:
if response.status != 200:
raise JokeApiError("API request failed")
data = response.json
if data.get("type") == 'NoSuchQuoteException':
raise NoSuchJoke(data.get("value", ""))
if data.get("type") != "success":
raise JokeApiError("API request failed")
raise Return(data["value"]["joke"])
except (JokeApiError, Return, StopIteration):
raise
except Exception as e:
raise JokeApiError() from e
def call_api_sync(it):
try:
for req in it:
response = requests.request(req.method, req.url, json=req.json)
it.send(
Response(status=response.status_code, json=response.json())
)
except Return as e:
return e.value
async def call_api_async(it):
async with ClientSession() as session:
try:
for req in it:
async with session.request(
method=req.method,
url=req.url,
json=req.json) as res:
json = await res.json()
response = Response(status=res.status, json=json)
it.send(response)
except Return as e:
return e.value
class JokeApiError(Exception):
pass
class NoSuchJoke(JokeApiError):
pass
class Return(Exception):
def __init__(self, value):
self.value = value
python python-3.x async-await
Python3's async/await syntax is great, but it does create a divide between libraries which are async-based and those which are not. For example, boto3 (AWS API library) currently doesn't work with async. There is a separate project, aiobotocore, attempts to recreate some of this functionality in an async context.
I have been thinking for a while about how to create HTTP API clients which can be run async and sync contexts. I have come up with a strategy that involves creating a separation between the logic (preparation of requests, interpretation of responses), and the sending. The logic is implemented as a generator function, which yields out an object which represents the request, and receives back an object representing the response. This generator is "run" by a runner function, which does the actual sending, and may be sync or async.
You can imagine that if, for example, boto3 had been written this way, it would be easy to re-use the bulk of the code to make an async version, rather than having to do an async rewrite.
I have included a toy example below. I would welcome comments.
from typing import Iterable, NamedTuple, Dict, Any, Optional
import requests
from aiohttp import ClientSession
URL_TEMPLATE = "https://api.icndb.com/jokes/id/"
class Request(NamedTuple):
method: str
url: str
json: Optional[Dict[str, Any]] = None
class Response(NamedTuple):
status: int
json: Optional[Dict[str, Any]] = None
def get_joke(id: int) -> Iterable[Request]:
response = yield Request("GET", URL_TEMPLATE.format(id=id))
try:
if response.status != 200:
raise JokeApiError("API request failed")
data = response.json
if data.get("type") == 'NoSuchQuoteException':
raise NoSuchJoke(data.get("value", ""))
if data.get("type") != "success":
raise JokeApiError("API request failed")
raise Return(data["value"]["joke"])
except (JokeApiError, Return, StopIteration):
raise
except Exception as e:
raise JokeApiError() from e
def call_api_sync(it):
try:
for req in it:
response = requests.request(req.method, req.url, json=req.json)
it.send(
Response(status=response.status_code, json=response.json())
)
except Return as e:
return e.value
async def call_api_async(it):
async with ClientSession() as session:
try:
for req in it:
async with session.request(
method=req.method,
url=req.url,
json=req.json) as res:
json = await res.json()
response = Response(status=res.status, json=json)
it.send(response)
except Return as e:
return e.value
class JokeApiError(Exception):
pass
class NoSuchJoke(JokeApiError):
pass
class Return(Exception):
def __init__(self, value):
self.value = value
python python-3.x async-await
edited Jun 28 at 12:06
Ludisposed
5,68621656
5,68621656
asked Jun 28 at 11:48
samfrances
17516
17516
I'd suggest adding aif name...section as well
â hjpotter92
Jun 28 at 17:23
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13
add a comment |Â
I'd suggest adding aif name...section as well
â hjpotter92
Jun 28 at 17:23
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13
I'd suggest adding a
if name... section as wellâ hjpotter92
Jun 28 at 17:23
I'd suggest adding a
if name... section as wellâ hjpotter92
Jun 28 at 17:23
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f197425%2fcreating-api-clients-that-are-async-agnostic%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
I'd suggest adding a
if name...section as wellâ hjpotter92
Jun 28 at 17:23
I'm not sure I understand. Could you elaborate?
â samfrances
Jun 28 at 18:13