Package: aiohttp-asynctools
Automatically add session management to a classSome function and classes to help you deal with aiohttp client sessions. This is made after thisdiscussion. This works for decorating both coroutines and asynchronous generators methods.Installationpipinstallaiohttp-asynctoolsUsageAutomatically attach anaiohttp.ClientSessionobject to your class in a fast and clean way with the following 3 steps:ImportasynctoolsMake your class extendasynctools.AbstractSessionContainerDecorate asynchronous methods/generators with@asynctools.attach_sessionto attach asessionargument.Optionaly, you can also cutomize the instanciation ofAbstractSessionContainerin your__init__method.
Here is what it looks like for a simple example using amath API:importasynctools# 1.MATH_API_URL="http://api.mathjs.org/v4"classMathRequests(asynctools.AbstractSessionContainer):# 2.@asynctools.attach_session# 3.asyncdefget_text(self,url,params,session=None):asyncwithsession.get(url,params=params)asresponse:returnawaitresponse.text()asyncdefget_square(self,value):params={"expr":f"{value}^2"}returnawaitself.get_text(MATH_API_URL,params=params)You are now ready to instantiate aMathRequestscontext manager and start requesting the math service using a singleaiohttpsession(the session is hidden from theMathRequestsuser). Here is how you could build a math server using the new class (basically we wrote a wrapper for the Math API and now we expose our own API).fromaiohttpimportwebroutes=web.RouteTableDef()@routes.get('/squares')asyncdefsquares(request):values=request.query['values'].split(',')asyncwithMathRequests()asmaths:squares=awaitasyncio.gather(*(maths.get_square(v)forvinvalues))return{'values':values,'squares':squares,}maths_app=web.Application()maths_app.add_routes(routes)if__name__=="__main__":web.run_app(app)We are now ready to test:curl'http://localhost:8080/squares?values=1,2,3,4,5,6,7,8,9,10'Which should output:{"values":["1","2","3","4","5","6","7","8","9","10"],"results":["1","4","9","16","25","36","49","64","81","100"]}A more complete example can be found inexample.Details and explanationWhat?The goal is to help aiohttp users to build classes that will contain sessions object in an efficient/clean way.Why?If you want to build class that will make requests usingaiohttp client, at some point you'll have to deal with sessions.
Thequickstart guide for aiohttp clienthas an important note about them.importaiohttpasyncwithaiohttp.ClientSession()assession:asyncwithsession.get('http://httpbin.org/get')asresp:print(resp.status)print(awaitresp.text())...NoteDon’t create a session per request. Most likely you need a session per application which performs all requests altogether.More complex cases may require a session per site, e.g. one for Github and other one for Facebook APIs. Anyway making a session for every request is avery badidea.A session contains a connection pool inside. Connection reusage and keep-alives (both are on by default) may speed up total performance.The goal is to have a single session attached to a given object, this is what this module offers with only 3 (very simple) lines of code.How?The module provides an abstract classAbstractSessionContainerand a method decoratorattach_sessionthat you'll have to use to automatically add session management to an existing class.Say you have a classMathRequeststhat has a single methodget_squarethat returns the square value of the given parameter using anaiohttp.getrequest to the math API service located athttp://api.mathjs.org/v4. Here is what your class look like for now:importasyncio,aiohttproutes=aiohttp.web.RouteTableDef()classMathRequests:asyncdefget_text(self,url,params):# Remember "making a session for every request is a very bad idea"asyncwithaiohttp.ClientSession()assession:asyncwithsession.get(url,params=params)asresponse:returnawaitresponse.text()asyncdefget_square(self,value):returnawaitself.get_text("http://api.mathjs.org/v4",params={'expr':'{}^2'.format(value)})@routes.get('/squares')asyncdefindex(request):tasks=[]data=request.queryvalues=data['values'].split(',')maths=MathRequests()results=awaitasyncio.gather(*[maths.get_square(v)forvinvalues])returnweb.json_response({'values':values,'results':results})maths_app=aiohttp.web.Application()maths_app.add_routes(routes)Asaiohttpdocumentation says, this is a bad idea to implementMathRequeststhis way, we need to share a single session for allget_squarerequests.A simple solution to this would be to store a client session object withinMathRequests, which you could initiate in the__init__method. Saddly this is not a very clean solution as aiohttp sessions should be instantiated in a synchronous way (outside the even loop). Seeaiohttp#1468for more information aboutcreation a session outside of coroutine.Here is the final solution using the provided moduleasynctools:importasyncioimportasynctools# 1) Import# 2) Extends the abstract class that will handle the aiohttp session for you:classMathRequests(asynctools.AbstractSessionContainer):def__init__(self):# 2') (optional) initilise with any 'aiohttp.ClientSession' argumentsuper().__init__(raise_for_status=True)# 3) This decorator will automatically fill the session argument:@asynctools.attach_sessionasyncdefget_text(self,url,params,session=None):# 4) Add the 'session' argumentasyncwithsession.get(url,params=params)asresponse:returnawaitresponse.text()asyncdefget_square(self,value):returnawaitself.get_text("http://api.mathjs.org/v4",params={'expr':'{}^2'.format(value)})fromaiohttpimportwebroutes=web.RouteTableDef()@routes.get('/squares')asyncdefindex(request):tasks=[]data=request.queryvalues=data['values'].split(',')asyncwithMathRequests()asmaths:# Use the object as a context manager (async with <context_manager> as <name>)results=awaitasyncio.gather(*[maths.get_square(v)forvinvalues])returnweb.json_response({'values':values,'results':results})maths_app=web.Application()maths_app.add_routes(routes)Using theMathRequestsas acontext manageris the best option (as it will make sure the session is correctly started and closed), but it's not the only option, you can also keep your code as it was before:@routes.get('/squares')asyncdefindex(request):tasks=[]data=request.queryvalues=data['values'].split(',')maths=MathRequests()results=awaitasyncio.gather(*[maths.get_square(v)forvinvalues])returnweb.json_response({'values':values,'results':results})In this case, no session is attached to themathsobject and every call toget_squarewill use a different session (which is as bad as it was with the old version ofMathRequests). What you can do to avoid that is toexplicitly open a "math session"which will make allget_squarecalls to use the same session (also, don't forget to close the session when you are done):@routes.get('/maths')asyncdefindex(request):tasks=[]data=request.queryvalues=data['values'].split(',')maths=MathRequests()maths.start_session()results=awaitasyncio.gather(*[maths.get_square(v)forvinvalues])maths.close_session()returnweb.json_response({'values':values,'results':results})