Implement asyncio support, plus session optimizations#87
Implement asyncio support, plus session optimizations#87seandstewart wants to merge 16 commits intocontentful:masterfrom
Conversation
…ll supported python interpreters.
|
@rubydog I'd appreciate a review when you have time, this is a major blocker for my company's onboarding. Here is the test output below: |
|
Hi @seandstewart, thanks for the PR. I will take a look at it and try to get back to you by next week. Cheers |
| def initialize(self): | ||
| raise NotImplementedError() | ||
|
|
||
| def teardown(self): | ||
| raise NotImplementedError() |
There was a problem hiding this comment.
Introduced an initializer method and a clean up method - these should be called by the developer at app startup and app shutdown.
| def _get(self, url: str, query: QueryT | None = None): | ||
| """ | ||
| Wrapper for the HTTP Request, | ||
| Rate Limit Backoff is handled here, | ||
| Responses are Processed with ResourceBuilder. | ||
| """ | ||
| raise NotImplementedError() | ||
|
|
||
| def _cache_content_types(self): | ||
| """ | ||
| Updates the Content Type Cache. | ||
| """ | ||
|
|
||
| raise NotImplementedError() |
There was a problem hiding this comment.
Implementation details handled by subclasses.
| @property | ||
| def client_info(self) -> ClientInfo: | ||
| if self._client_info is None: | ||
| self._client_info = self._get_client_info() | ||
|
|
||
| return self._client_info | ||
|
|
||
| @property | ||
| def headers(self) -> dict[str, str]: | ||
| if self._headers is None: | ||
| self._headers = self._request_headers() | ||
|
|
||
| return self._headers | ||
|
|
||
| @property | ||
| def proxy_info(self) -> abstract.ProxyInfo: | ||
| if self._proxy_info is None: | ||
| self._proxy_info = self._proxy_parameters() | ||
|
|
||
| return self._proxy_info |
There was a problem hiding this comment.
These were re-created with every request to Contentful. We're now only generating this information once, which is much more efficient.
| @property | ||
| def transport(self) -> abstract.AbstractTransport: | ||
| if self._transport is None: | ||
| self._transport = self._get_transport() | ||
|
|
||
| return self._transport | ||
|
|
||
| def qualified_url(self) -> str: | ||
| scheme = "https://" if self.https else "http://" | ||
| hostname = self.api_url | ||
| if hostname.startswith("http"): | ||
| scheme = "" | ||
|
|
||
| path = f"/spaces/{self.space_id}/environments/{self.environment}/" | ||
| url = f"{scheme}{hostname}{path}" | ||
| return url | ||
|
|
||
| def _get_transport(self) -> abstract.AbstractTransport: | ||
| base_url = self.qualified_url() | ||
| transport = self.transport_cls( | ||
| base_url=base_url, | ||
| timeout_s=self.timeout_s, | ||
| proxy_info=self.proxy_info, | ||
| default_headers=self.headers, | ||
| max_retries=self.max_rate_limit_retries, | ||
| max_retry_wait_seconds=self.max_rate_limit_wait, | ||
| ) | ||
| return transport |
There was a problem hiding this comment.
We initialize the transport lazily and provide the fully-qualified URL to the targeted Contentful space/env as a base_url to the Transport.
| def _format_params(self, query: QueryT | None) -> dict[str, str]: | ||
| query = query or {} | ||
| params = queries.normalize(**query) | ||
| if not self.authorization_as_header: | ||
| params["access_token"] = self.access_token | ||
|
|
||
| return params |
There was a problem hiding this comment.
Isolated query-string formatting into a more efficient, generic normalizer.
| __all__ = ( | ||
| "Client", | ||
| "AsyncClient", | ||
| "Entry", | ||
| "Asset", | ||
| "Space", | ||
| "Locale", | ||
| "Link", | ||
| "ContentType", | ||
| "DeletedAsset", | ||
| "DeletedEntry", | ||
| "ContentTypeCache", | ||
| "ContentTypeField", | ||
| ) | ||
|
|
||
| _metadata = metadata.metadata(__package__) | ||
|
|
||
| __version__ = _metadata.get("version") | ||
| __author__ = _metadata.get("author") | ||
| __email__ = _metadata.get("author-email") |
There was a problem hiding this comment.
Defining an __all__ and using importlib.metadata to get package metadata.
| ) | ||
| logger.debug( | ||
| f"{prefix}{retry_message}", | ||
| extra={"tries": tries, "reset_time": reset_time}, |
There was a problem hiding this comment.
Passing in extra= will work well with structured logging for app developers.
| def update_cache(cls, *, space_id: str, content_types: list[ContentType]): | ||
| """ | ||
| Updates the Cache with all Content Types from the Space. | ||
| """ | ||
|
|
||
| cls.__CACHE__[client.space_id] = client.content_types() | ||
| cls.__CACHE__[space_id] = content_types |
There was a problem hiding this comment.
Breaking change: Can't implicitly call the client - interface now requires that you call the client and pass in the result.
There was a problem hiding this comment.
Copied from test_client and updated for async.
| error = errors.get_error_for_status_code( | ||
| response.status_code, | ||
| content=response.content, | ||
| headers=response.headers, | ||
| ) |
There was a problem hiding this comment.
Only major update was to migrate to the new function.
@rubydog Sounds good. I've gone through and added a self-review! |
|
Hello, any status update for this PR? |
|
Hi @pdelagrave, We are currently discussing internally whether we want to support asyncio. Given the scope of this PR, it will take me some time to review it thoroughly. Apologies for the delay. |
This change adds support for asyncio. In order to do so, I made the following changes:
resolves #86