From bcb9560a8ccc571d32c2af61162f9145e20d7de6 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 7 Aug 2023 13:35:33 -0700 Subject: [PATCH 1/9] add async client --- protoc-gen-twirpy/generator/template.go | 20 +++++++++- setup.py | 3 ++ twirp/async_client.py | 50 +++++++++++++++++++++++++ twirp/client.py | 45 ++-------------------- twirp/exceptions.py | 39 +++++++++++++++++++ 5 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 twirp/async_client.py diff --git a/protoc-gen-twirpy/generator/template.go b/protoc-gen-twirpy/generator/template.go index 1ed0f57..a20d256 100644 --- a/protoc-gen-twirpy/generator/template.go +++ b/protoc-gen-twirpy/generator/template.go @@ -38,6 +38,10 @@ from google.protobuf import symbol_database as _symbol_database from twirp.base import Endpoint from twirp.server import TwirpServer from twirp.client import TwirpClient +try: + from twirp.async_clint import AsyncTwirpClient +except ModuleNotFoundError: + AsyncTwirpClient = None _sym_db = _symbol_database.Default() {{range .Services}} @@ -66,4 +70,18 @@ class {{.Name}}Client(TwirpClient): response_obj=_sym_db.GetSymbol("{{.Output}}"), **kwargs, ) -{{end}}{{end}}`)) +{{end}} + +if AsyncTwirpClient: + class Async{{.Name}}Client(AsyncTwirpClient): + {{range .Methods}} + async def {{.Name}}(self, *args, ctx, request, server_path_prefix="/twirp", **kwargs): + return await self._make_request( + url=F"{server_path_prefix}/{{.ServiceURL}}/{{.Name}}", + ctx=ctx, + request=request, + response_obj=_sym_db.GetSymbol("{{.Output}}"), + **kwargs, + ) + {{end}} +{{end}}`)) diff --git a/setup.py b/setup.py index 1ff0252..1b4c415 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,9 @@ 'structlog', 'protobuf' ], + extras_require={ + 'async': ['aiohttp'], + }, test_requires=[ ], zip_safe=False) diff --git a/twirp/async_client.py b/twirp/async_client.py new file mode 100644 index 0000000..44310c7 --- /dev/null +++ b/twirp/async_client.py @@ -0,0 +1,50 @@ +import json +import aiohttp + +from . import exceptions +from . import errors + + +class AsyncTwirpClient: + def __init__(self, address, timeout=5): + self._address = address + self._timeout = timeout + self._http_session = None + + @property + def http_session(self) -> aiohttp.ClientSession: + if self._http_session is None: + self._http_session = aiohttp.ClientSession( + self._address, timeout=aiohttp.ClientTimeout(total=self._timeout)) + return self._http_session + + async def _make_request(self, *args, url, ctx, request, response_obj, **kwargs): + headers = ctx.get_headers() + if 'headers' in kwargs: + headers.update(kwargs['headers']) + kwargs['headers'] = headers + kwargs['headers']['Content-Type'] = 'application/protobuf' + try: + resp = await self.http_session.post(url=url, data=request.SerializeToString(), **kwargs) + if resp.status == 200: + response = response_obj() + response.ParseFromString(resp.content) + return response + try: + raise exceptions.TwirpServerException.from_json(await resp.json()) + except (aiohttp.ContentTypeError, json.JSONDecodeError): + raise exceptions.twirp_error_from_intermediary( + resp.status, resp.reason, resp.headers, await resp.text) from None + # Todo: handle error + except aiohttp.ServerTimeoutError as e: + raise exceptions.TwirpServerException( + code=errors.Errors.DeadlineExceeded, + message=str(e), + meta={"original_exception": e}, + ) + except aiohttp.ServerConnectionError as e: + raise exceptions.TwirpServerException( + code=errors.Errors.Unavailable, + message=str(e), + meta={"original_exception": e}, + ) diff --git a/twirp/client.py b/twirp/client.py index fe34fc1..1fc9f8a 100644 --- a/twirp/client.py +++ b/twirp/client.py @@ -1,4 +1,3 @@ -import json import requests from . import exceptions @@ -25,8 +24,9 @@ def _make_request(self, *args, url, ctx, request, response_obj, **kwargs): return response try: raise exceptions.TwirpServerException.from_json(resp.json()) - except json.JSONDecodeError: - raise self._twirp_error_from_intermediary(resp) from None + except requests.JSONDecodeError: + raise exceptions.twirp_error_from_intermediary( + resp.status_code, resp.reason, resp.headers, resp.text) from None # Todo: handle error except requests.exceptions.Timeout as e: raise exceptions.TwirpServerException( @@ -40,42 +40,3 @@ def _make_request(self, *args, url, ctx, request, response_obj, **kwargs): message=str(e), meta={"original_exception": e}, ) - - @staticmethod - def _twirp_error_from_intermediary(resp): - # see https://twitchtv.github.io/twirp/docs/errors.html#http-errors-from-intermediary-proxies - meta = { - 'http_error_from_intermediary': 'true', - 'status_code': str(resp.status_code), - } - - if resp.is_redirect: - # twirp uses POST which should not redirect - code = errors.Errors.Internal - location = resp.headers.get('location') - message = 'unexpected HTTP status code %d "%s" received, Location="%s"' % ( - resp.status_code, - resp.reason, - location, - ) - meta['location'] = location - - else: - code = { - 400: errors.Errors.Internal, # JSON response should have been returned - 401: errors.Errors.Unauthenticated, - 403: errors.Errors.PermissionDenied, - 404: errors.Errors.BadRoute, - 429: errors.Errors.ResourceExhausted, - 502: errors.Errors.Unavailable, - 503: errors.Errors.Unavailable, - 504: errors.Errors.Unavailable, - }.get(resp.status_code, errors.Errors.Unknown) - - message = 'Error from intermediary with HTTP status code %d "%s"' % ( - resp.status_code, - resp.reason, - ) - meta['body'] = resp.text - - return exceptions.TwirpServerException(code=code, message=message, meta=meta) diff --git a/twirp/exceptions.py b/twirp/exceptions.py index fd5f59f..ec3d7f0 100644 --- a/twirp/exceptions.py +++ b/twirp/exceptions.py @@ -72,3 +72,42 @@ def RequiredArgument(*args, argument): argument=argument, error="is required" ) + + +def twirp_error_from_intermediary(status, reason, headers, body): + # see https://twitchtv.github.io/twirp/docs/errors.html#http-errors-from-intermediary-proxies + meta = { + 'http_error_from_intermediary': 'true', + 'status_code': str(status), + } + + if 300 <= status < 400: + # twirp uses POST which should not redirect + code = errors.Errors.Internal + location = headers.get('location') + message = 'unexpected HTTP status code %d "%s" received, Location="%s"' % ( + status, + reason, + location, + ) + meta['location'] = location + + else: + code = { + 400: errors.Errors.Internal, # JSON response should have been returned + 401: errors.Errors.Unauthenticated, + 403: errors.Errors.PermissionDenied, + 404: errors.Errors.BadRoute, + 429: errors.Errors.ResourceExhausted, + 502: errors.Errors.Unavailable, + 503: errors.Errors.Unavailable, + 504: errors.Errors.Unavailable, + }.get(status, errors.Errors.Unknown) + + message = 'Error from intermediary with HTTP status code %d "%s"' % ( + status, + reason, + ) + meta['body'] = body + + return TwirpServerException(code=code, message=message, meta=meta) From 53c38c95600405620143b0b40a5e9799cf5f2f45 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 7 Aug 2023 23:21:46 -0700 Subject: [PATCH 2/9] create/close single aiohttp session --- twirp/async_client.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/twirp/async_client.py b/twirp/async_client.py index 44310c7..1264ec6 100644 --- a/twirp/async_client.py +++ b/twirp/async_client.py @@ -1,41 +1,47 @@ +import asyncio import json import aiohttp from . import exceptions from . import errors - class AsyncTwirpClient: def __init__(self, address, timeout=5): self._address = address self._timeout = timeout - self._http_session = None + self._session = None + + def __del__(self): + if self._session: + asyncio.create_task(self._session.close()) @property - def http_session(self) -> aiohttp.ClientSession: - if self._http_session is None: - self._http_session = aiohttp.ClientSession( + def session(self): + if self._session is None: + self._session = aiohttp.ClientSession( self._address, timeout=aiohttp.ClientTimeout(total=self._timeout)) - return self._http_session + return self._session - async def _make_request(self, *args, url, ctx, request, response_obj, **kwargs): + async def _make_request(self, *, url, ctx, request, response_obj, session=None, **kwargs): headers = ctx.get_headers() if 'headers' in kwargs: headers.update(kwargs['headers']) kwargs['headers'] = headers kwargs['headers']['Content-Type'] = 'application/protobuf' try: - resp = await self.http_session.post(url=url, data=request.SerializeToString(), **kwargs) - if resp.status == 200: - response = response_obj() - response.ParseFromString(resp.content) - return response - try: - raise exceptions.TwirpServerException.from_json(await resp.json()) - except (aiohttp.ContentTypeError, json.JSONDecodeError): - raise exceptions.twirp_error_from_intermediary( - resp.status, resp.reason, resp.headers, await resp.text) from None - # Todo: handle error + async with await (session or self.session).post( + url=url, data=request.SerializeToString(), **kwargs + ) as resp: + if resp.status == 200: + response = response_obj() + response.ParseFromString(await resp.read()) + return response + try: + raise exceptions.TwirpServerException.from_json(await resp.json()) + except (aiohttp.ContentTypeError, json.JSONDecodeError): + raise exceptions.twirp_error_from_intermediary( + resp.status, resp.reason, resp.headers, await resp.text() + ) from None except aiohttp.ServerTimeoutError as e: raise exceptions.TwirpServerException( code=errors.Errors.DeadlineExceeded, From e648d9e2a8f5e5e48ab9b5ef6ff462a392d131fe Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 7 Aug 2023 23:22:56 -0700 Subject: [PATCH 3/9] update template --- protoc-gen-twirpy/generator/template.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/protoc-gen-twirpy/generator/template.go b/protoc-gen-twirpy/generator/template.go index a20d256..e4bd968 100644 --- a/protoc-gen-twirpy/generator/template.go +++ b/protoc-gen-twirpy/generator/template.go @@ -39,9 +39,10 @@ from twirp.base import Endpoint from twirp.server import TwirpServer from twirp.client import TwirpClient try: - from twirp.async_clint import AsyncTwirpClient + from twirp.async_client import AsyncTwirpClient + _async_available = True except ModuleNotFoundError: - AsyncTwirpClient = None + _async_available = False _sym_db = _symbol_database.Default() {{range .Services}} @@ -72,16 +73,16 @@ class {{.Name}}Client(TwirpClient): ) {{end}} -if AsyncTwirpClient: +if _async_available: class Async{{.Name}}Client(AsyncTwirpClient): - {{range .Methods}} - async def {{.Name}}(self, *args, ctx, request, server_path_prefix="/twirp", **kwargs): +{{range .Methods}} + async def {{.Name}}(self, *, ctx, request, server_path_prefix="/twirp", session=None, **kwargs): return await self._make_request( url=F"{server_path_prefix}/{{.ServiceURL}}/{{.Name}}", ctx=ctx, request=request, response_obj=_sym_db.GetSymbol("{{.Output}}"), + session=session, **kwargs, ) - {{end}} -{{end}}`)) +{{end}}{{end}}`)) From 6d270a3fd71bc8e912e8eaea41d3c99c48b8a9c2 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 7 Aug 2023 23:36:21 -0700 Subject: [PATCH 4/9] update example --- example/client.py | 53 +++++++++++++++++++++----- example/generated/haberdasher_twirp.py | 19 +++++++++ example/requirements.txt | 4 +- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/example/client.py b/example/client.py index 5b8fd78..1df7691 100644 --- a/example/client.py +++ b/example/client.py @@ -1,15 +1,50 @@ +try: + import asyncio +except ModuleNotFoundError: + pass + from twirp.context import Context from twirp.exceptions import TwirpServerException from generated import haberdasher_twirp, haberdasher_pb2 -client = haberdasher_twirp.HaberdasherClient("http://localhost:3000") -try: - response = client.MakeHat( - ctx=Context(), request=haberdasher_pb2.Size(inches=12), server_path_prefix="/twirpy") - if not response.HasField('name'): - print("We didn't get a name!") - print(response) -except TwirpServerException as e: - print(e.code, e.message, e.meta, e.to_dict()) +def main(): + client = haberdasher_twirp.HaberdasherClient("http://localhost:3000") + + try: + response = client.MakeHat( + ctx=Context(), + request=haberdasher_pb2.Size(inches=12), + server_path_prefix="/twirpy", + ) + if not response.HasField("name"): + print("We didn't get a name!") + print(response) + except TwirpServerException as e: + print(e.code, e.message, e.meta, e.to_dict()) + + +async def async_main(): + client = haberdasher_twirp.AsyncHaberdasherClient("http://localhost:3000") + + try: + response = await client.MakeHat( + ctx=Context(), + request=haberdasher_pb2.Size(inches=12), + server_path_prefix="/twirpy", + ) + if not response.HasField("name"): + print("We didn't get a name!") + print(response) + except TwirpServerException as e: + print(e.code, e.message, e.meta, e.to_dict()) + + +if __name__ == "__main__": + if hasattr(haberdasher_twirp, "AsyncHaberdasherClient"): + print("using async client") + loop = asyncio.get_event_loop() + loop.run_until_complete(async_main()) + else: + main() diff --git a/example/generated/haberdasher_twirp.py b/example/generated/haberdasher_twirp.py index 42fb488..019450b 100644 --- a/example/generated/haberdasher_twirp.py +++ b/example/generated/haberdasher_twirp.py @@ -7,6 +7,11 @@ from twirp.base import Endpoint from twirp.server import TwirpServer from twirp.client import TwirpClient +try: + from twirp.async_client import AsyncTwirpClient + _async_available = True +except ModuleNotFoundError: + _async_available = False _sym_db = _symbol_database.Default() @@ -35,3 +40,17 @@ def MakeHat(self, *args, ctx, request, server_path_prefix="/twirp", **kwargs): response_obj=_sym_db.GetSymbol("twitch.twirp.example.Hat"), **kwargs, ) + + +if _async_available: + class AsyncHaberdasherClient(AsyncTwirpClient): + + async def MakeHat(self, *, ctx, request, server_path_prefix="/twirp", session=None, **kwargs): + return await self._make_request( + url=F"{server_path_prefix}/twitch.twirp.example.Haberdasher/MakeHat", + ctx=ctx, + request=request, + response_obj=_sym_db.GetSymbol("twitch.twirp.example.Hat"), + session=session, + **kwargs, + ) diff --git a/example/requirements.txt b/example/requirements.txt index afa8e88..b73afa1 100644 --- a/example/requirements.txt +++ b/example/requirements.txt @@ -1,2 +1,2 @@ -twirp==0.0.2 -uvicorn==0.12.2 +twirp==0.0.8 +uvicorn==0.23.2 From f54d2fd84ddc22baa36821a8b8d11fb38b169451 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Tue, 8 Aug 2023 00:14:15 -0700 Subject: [PATCH 5/9] fix bug in missing content-type exception --- twirp/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twirp/base.py b/twirp/base.py index 6417206..ba0fbfa 100644 --- a/twirp/base.py +++ b/twirp/base.py @@ -101,6 +101,6 @@ def _get_encoder_decoder(self, endpoint, headers): else: raise exceptions.TwirpServerException( code=errors.Errors.BadRoute, - message="unexpected Content-Type: " + ctype + message="unexpected Content-Type: " + str(ctype) ) return encoder, decoder From 814dff4db913330a54f35afc0c88916c7ca0306c Mon Sep 17 00:00:00 2001 From: chadawagner Date: Tue, 8 Aug 2023 12:08:17 -0700 Subject: [PATCH 6/9] session param in init, better teardown and examples --- example/client.py | 44 +++++++++++++++++++++++++++++++++++++++---- twirp/async_client.py | 21 +++++++++++++++------ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/example/client.py b/example/client.py index 1df7691..02272e7 100644 --- a/example/client.py +++ b/example/client.py @@ -1,5 +1,6 @@ try: import asyncio + import aiohttp except ModuleNotFoundError: pass @@ -9,8 +10,12 @@ from generated import haberdasher_twirp, haberdasher_pb2 +server_url = "http://localhost:3000" +timeout_s = 5 + + def main(): - client = haberdasher_twirp.HaberdasherClient("http://localhost:3000") + client = haberdasher_twirp.HaberdasherClient(server_url, timeout_s) try: response = client.MakeHat( @@ -26,25 +31,56 @@ def main(): async def async_main(): - client = haberdasher_twirp.AsyncHaberdasherClient("http://localhost:3000") + client = haberdasher_twirp.AsyncHaberdasherClient(server_url, timeout_s) + + try: + response = await client.MakeHat( + ctx=Context(), + request=haberdasher_pb2.Size(inches=12), + server_path_prefix="/twirpy", + ) + if not response.HasField("name"): + print("We didn't get a name!") + print(response) + except TwirpServerException as e: + print(e.code, e.message, e.meta, e.to_dict()) + + +async def async_with_session(): + # It is optional but recommended to provide your own ClientSession to the twirp client + # either on init or per request, and ensure it is closed properly on app shutdown. + # Otherwise, the client will create its own session to use, which it will attempt to + # close in its __del__ method, but has no control over how or when that will get called. + + # NOTE: ClientSession may only be created (or closed) within a coroutine. + session = aiohttp.ClientSession( + server_url, timeout=aiohttp.ClientTimeout(total=timeout_s) + ) + + # If session is provided, session controls the timeout. Timeout parameter to client init is unused + client = haberdasher_twirp.AsyncHaberdasherClient(server_url, session=session) try: response = await client.MakeHat( ctx=Context(), request=haberdasher_pb2.Size(inches=12), server_path_prefix="/twirpy", + # Optionally provide a session per request + # session=session, ) if not response.HasField("name"): print("We didn't get a name!") print(response) except TwirpServerException as e: print(e.code, e.message, e.meta, e.to_dict()) + finally: + # Close the session (could also use a context manager) + await session.close() if __name__ == "__main__": if hasattr(haberdasher_twirp, "AsyncHaberdasherClient"): print("using async client") - loop = asyncio.get_event_loop() - loop.run_until_complete(async_main()) + asyncio.run(async_main()) else: main() diff --git a/twirp/async_client.py b/twirp/async_client.py index 1264ec6..31bca4e 100644 --- a/twirp/async_client.py +++ b/twirp/async_client.py @@ -6,20 +6,29 @@ from . import errors class AsyncTwirpClient: - def __init__(self, address, timeout=5): + def __init__(self, address, timeout=5, session=None): self._address = address self._timeout = timeout - self._session = None + self._session = session + self._should_close_session = False def __del__(self): - if self._session: - asyncio.create_task(self._session.close()) + if self._should_close_session: + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(self._session.close()) + elif not loop.is_closed(): + loop.run_until_complete(self._session.close()) + except RuntimeError: + pass @property def session(self): if self._session is None: self._session = aiohttp.ClientSession( self._address, timeout=aiohttp.ClientTimeout(total=self._timeout)) + self._should_close_session = True return self._session async def _make_request(self, *, url, ctx, request, response_obj, session=None, **kwargs): @@ -42,10 +51,10 @@ async def _make_request(self, *, url, ctx, request, response_obj, session=None, raise exceptions.twirp_error_from_intermediary( resp.status, resp.reason, resp.headers, await resp.text() ) from None - except aiohttp.ServerTimeoutError as e: + except asyncio.TimeoutError as e: raise exceptions.TwirpServerException( code=errors.Errors.DeadlineExceeded, - message=str(e), + message=str(e) or "request timeout", meta={"original_exception": e}, ) except aiohttp.ServerConnectionError as e: From 744006e14b2c0e2a7f9a16312fdfd0c8c2116917 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Tue, 8 Aug 2023 12:10:24 -0700 Subject: [PATCH 7/9] formatting --- twirp/async_client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/twirp/async_client.py b/twirp/async_client.py index 31bca4e..2f6b019 100644 --- a/twirp/async_client.py +++ b/twirp/async_client.py @@ -5,6 +5,7 @@ from . import exceptions from . import errors + class AsyncTwirpClient: def __init__(self, address, timeout=5, session=None): self._address = address @@ -27,16 +28,19 @@ def __del__(self): def session(self): if self._session is None: self._session = aiohttp.ClientSession( - self._address, timeout=aiohttp.ClientTimeout(total=self._timeout)) + self._address, timeout=aiohttp.ClientTimeout(total=self._timeout) + ) self._should_close_session = True return self._session - async def _make_request(self, *, url, ctx, request, response_obj, session=None, **kwargs): + async def _make_request( + self, *, url, ctx, request, response_obj, session=None, **kwargs + ): headers = ctx.get_headers() - if 'headers' in kwargs: - headers.update(kwargs['headers']) - kwargs['headers'] = headers - kwargs['headers']['Content-Type'] = 'application/protobuf' + if "headers" in kwargs: + headers.update(kwargs["headers"]) + kwargs["headers"] = headers + kwargs["headers"]["Content-Type"] = "application/protobuf" try: async with await (session or self.session).post( url=url, data=request.SerializeToString(), **kwargs From 15daba46eaac99f31076f519e0ad4fa4ecbaa2b9 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 18 Sep 2023 18:08:54 -0700 Subject: [PATCH 8/9] don't auto-create client session --- twirp/async_client.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/twirp/async_client.py b/twirp/async_client.py index 2f6b019..6710ed7 100644 --- a/twirp/async_client.py +++ b/twirp/async_client.py @@ -1,5 +1,7 @@ import asyncio import json +from typing import Optional + import aiohttp from . import exceptions @@ -7,31 +9,11 @@ class AsyncTwirpClient: - def __init__(self, address, timeout=5, session=None): + def __init__( + self, address: str, session: Optional[aiohttp.ClientSession] = None + ) -> None: self._address = address - self._timeout = timeout self._session = session - self._should_close_session = False - - def __del__(self): - if self._should_close_session: - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - loop.create_task(self._session.close()) - elif not loop.is_closed(): - loop.run_until_complete(self._session.close()) - except RuntimeError: - pass - - @property - def session(self): - if self._session is None: - self._session = aiohttp.ClientSession( - self._address, timeout=aiohttp.ClientTimeout(total=self._timeout) - ) - self._should_close_session = True - return self._session async def _make_request( self, *, url, ctx, request, response_obj, session=None, **kwargs @@ -41,8 +23,14 @@ async def _make_request( headers.update(kwargs["headers"]) kwargs["headers"] = headers kwargs["headers"]["Content-Type"] = "application/protobuf" + + if session is None: + session = self._session + if not isinstance(session, aiohttp.ClientSession): + raise TypeError(f"invalid session type '{type(session).__name__}'") + try: - async with await (session or self.session).post( + async with await session.post( url=url, data=request.SerializeToString(), **kwargs ) as resp: if resp.status == 200: From 183e5ff0f5c98cb63521945749d9518d1dad2203 Mon Sep 17 00:00:00 2001 From: chadawagner Date: Mon, 18 Sep 2023 18:12:47 -0700 Subject: [PATCH 9/9] update example --- example/client.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/example/client.py b/example/client.py index 02272e7..d2df794 100644 --- a/example/client.py +++ b/example/client.py @@ -31,33 +31,13 @@ def main(): async def async_main(): - client = haberdasher_twirp.AsyncHaberdasherClient(server_url, timeout_s) - - try: - response = await client.MakeHat( - ctx=Context(), - request=haberdasher_pb2.Size(inches=12), - server_path_prefix="/twirpy", - ) - if not response.HasField("name"): - print("We didn't get a name!") - print(response) - except TwirpServerException as e: - print(e.code, e.message, e.meta, e.to_dict()) - - -async def async_with_session(): - # It is optional but recommended to provide your own ClientSession to the twirp client + # The caller must provide their own ClientSession to the twirp client # either on init or per request, and ensure it is closed properly on app shutdown. - # Otherwise, the client will create its own session to use, which it will attempt to - # close in its __del__ method, but has no control over how or when that will get called. # NOTE: ClientSession may only be created (or closed) within a coroutine. session = aiohttp.ClientSession( server_url, timeout=aiohttp.ClientTimeout(total=timeout_s) ) - - # If session is provided, session controls the timeout. Timeout parameter to client init is unused client = haberdasher_twirp.AsyncHaberdasherClient(server_url, session=session) try: