Skip to content
This repository was archived by the owner on Sep 12, 2024. It is now read-only.
This repository was archived by the owner on Sep 12, 2024. It is now read-only.

Suggestion: use of an async request framework (as replacement for requests) #306

@cglacet

Description

@cglacet

Is your feature request related to a problem? Please describe.
The issue is that many (most?) modern python web frameworks are working in an asynchronous fashion (ASGI). The current API you are offering is purely synchronous which make it
"impossible" to use with such frameworks.

Describe the solution you'd like
Most of the time I'm using aiohttp, its interface is very close to requests which makes it a good candidate.

Describe alternatives you've considered
I'm currently re-writing it for myself.

Remark

Also, that's a bit related so I will answer this remark from your code:

# duffel_api/client.py
class Duffel:
    """Client to the entire API"""

    def __init__(self, **kwargs):
        # TODO(nlopes): I really don't like how I've built this- we shouldn't keep
        # instantiating the HttpClient through class inheritance.
        # We should (maybe!) have a singleton pattern on HttpClient and use
        # composition in all of these instead.

        # Keep this as we use it when doing the lazy-evaluation of the different
        # clients
        self._kwargs = kwargs

I think it's not too much of a problem, the real problem is underlying. You are creating one session per init (and therefore per inherited class), the code looks a bit like this:

# duffel_api/http_client.py
class HttpClient:
    def __init__(self, access_token=None, api_url=None, api_version=None, **settings):
        self.http_session = Session()

I think you should spawn a single HTTP session for all APIs (unless there is a good reason not to). Then, you could use that unique session in all subclasses (passing it as argument to the HTTpClient). You don't even need to have a singleton for that an can simply use a lazy property directly in Duffel client, like so (the code is already using aiohttp because I've already rewrote this part):

# duffel_api/client.py
class Duffel:
    _session: Optional[aiohttp.ClientSession] = None

    def __init__(self, **kwargs):
        self._kwargs = kwargs
        self._session = None

    @property
    def http_session(self):
        if self._session is None:
            self._session = aiohttp.ClientSession()
        return self._session

    @lazy_property
    def aircraft(self):
        """Aircraft API - /air/aircraft"""
        return AircraftClient(**self._kwargs, session=self.http_session)
# duffel_api/http_client.py
class HttpClient:
    _session: aiohttp.ClientSession

    URL = "https://api.duffel.com"
    VERSION = "v1"

    def __init__(
        self,
        *,
        session: aiohttp.ClientSession,
        access_token: str = None,
        api_url: str = None,
        api_version: str = None,
        **settings,
    ):
        self._api_url = api_url or HttpClient.URL
        self._api_version = api_version or HttpClient.VERSION

        if not access_token:
            access_token = os.getenv("DUFFEL_ACCESS_TOKEN")
            if not access_token:
                raise ClientError("must set DUFFEL_ACCESS_TOKEN")

        self._headers = {
            "Authorization": f"Bearer {access_token}",
            "User-Agent": f"Duffel/{self._api_version} duffel_api_python/{version()}",
            "Accept": "application/json",
            "Duffel-Version": self._api_version,
        }
        self._settings = settings
        self.http_session = session

    async def _http_call(self, endpoint, method, query_params=None, body=None):
        request_url = self._api_url + endpoint
        request_args = dict(params=query_params, json=body, headers=self._headers, **self._settings)
        async with self.http_session.request(method, request_url, **request_args) as response:
            if response.status_code in {http_codes.ok, http_codes.created}:
                try:
                    return response.json()
                # etc.

This way, all HTTP requests will use the same session (single TCP handshake).

You could also directly offer the enduser the opportunity to start the session on its own (for example when the server starts).

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions