I am building own async python package and faced the problem.
This is my code:
class Client:
"""
Async client for making requests
"""
def __init__(self, base_url: str = BASE_URL) -> None:
self.base_url = base_url
self.session = ClientSession()
async def get(self, method: str, *args: tp.Any, **kwargs: tp.Any) -> tp.Any:
async with self.session.get(f'{self.base_url}/{method}', *args, **kwargs) as response:
data = await response.json()
return data
When I try to use something like this:
await client.get()
I get
RuntimeError: Timeout context manager should be used inside a task
I suppose that the reason of this error is calling ClientSession() not inside the coroutine. But I hope that somebody knows the way to re-use ClientSession()
I have already read other similar questions, but they are not suitable to my situation.
You can initialize (and cache) the session when needed:
class Client:
"""
Async client for making requests
"""
def __init__(self, base_url: str = BASE_URL) -> None:
self.base_url = base_url
self.session = None
async def get(self, method: str, *args: tp.Any, **kwargs: tp.Any) -> tp.Any:
if not self.session:
self.session = ClientSession()
async with self.session.get(f'{self.base_url}/{method}', *args, **kwargs) as response:
data = await response.json()
return data
Depending on how you use the Client you can also use a class attribute for the session object.
Update:
ClientSession creation should be protected from race condition using Mutex:
_session_mutex = asyncio.Lock()
async def __create_session_if_required(self):
if self.session is None:
async with self._session_mutex:
if self.session is None:
self.session = aiohttp.ClientSession()
# should be closed if not Singleton class: "await session.close()"
async def get(..):
await self.__create_session_if_required()
async with self.session.get() as response:
# ...
Related
I have a pretty complicated API with custom parameters and headers so I created a class to wrap around it. Here's a contrived example:
import asyncio
import aiohttp
# The wrapper class around my API
class MyAPI:
def __init__(self, base_url: str):
self.base_url = base_url
async def send(self, session, method, url) -> aiohttp.ClientResponse:
request_method = getattr(session, method.lower())
full_url = f"{self.base_url}/{url}"
async with request_method(full_url) as response:
return response
async def main():
api = MyAPI("https://httpbin.org")
async with aiohttp.ClientSession() as session:
response = await api.send(session, "GET", "/uuid")
print(response.status) # 200 OK
print(await response.text()) # Exception: Connection closed
asyncio.run(main())
Why is my session closed? I didn't exit the context manager of session.
If I ignore the wrapper class, everything works as expected:
async def main():
async with aiohttp.ClientSession() as session:
async with session.get("https://httpbin.org/uuid") as response:
print(await response.text())
You can't call response.text() once you have left the request_method(full_url) context.
If you write:
async with request_method(full_url) as response:
text = await response.text()
return response.status, text
then the send() method returns without error.
I'm making a python module for interacting with an API. I'd like it to be fast, so I chose to use asyncio and Aiohttp. I'm quite new to async programming and I'm not quite sure how to reuse the same session for every request. Also, I'd like to spare my end-users the hassle of creating the loop etc. I came up with this class for my base client:
import asyncio
import aiohttp
class BaseClient:
API_BASE_URL = "dummyURL"
API_VERSION = 3
async def __aenter__(self):
self._session = aiohttp.ClientSession(raise_for_status=True)
return self
async def __aexit__(self, exc_type, exc, tb):
await self._session.close()
#remove the next line when aiohttp 4.0 is released
await asyncio.sleep(0.250)
async def _get(self, endpoint: str) -> None:
url = f"{self.API_BASE_URL}/{endpoint}/?v={self.API_VERSION}"
async with self._session.get(url) as resp:
json_body = await resp.json()
return json_body
async def list_forums(self):
endpoint = "forums"
return await self._get(endpoint)
async def main():
async with BaseClient() as client:
forums = await client.list_forums()
print(forums)
asyncio.run(main())
Is that the right way to reuse the same session? Is it possible to refactor BaseClient in such a way my end-users would only have to dothe following:
client = BaseClient()
forums = client.list_forums()
Thanks for your help.
This is the sample code I am using. I wanted to quickly parse the pages and enter the resulting data into the database. However, after adding one line, my code started to run significantly slower. I understand that this is related to the work of the database. But I don't understand how to fix it.
If you have any other suggestions for speeding up this code, I would be grateful for your help.
import asyncpg
import asyncio
import aiohttp
from settings import URI, SQL, URLS
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class DBManager(metaclass=Singleton):
""" Class for interacting with Postgres database """
def __init__(self, dsn) -> None:
""" The constructor takes the database """
self.dsn = dsn
self.pool = None
async def connect(self):
self.pool = await asyncpg.create_pool(dsn=self.dsn)
async def insert_data(self, peson: str, address: str):
async with self.pool.acquire() as connect:
return await connect.execute(SQL, type_tx, address)
db = DBManager(URI)
async def check_address(url, session):
async with session.get(url) as result:
try:
result = await result.json()
person = 'adult' if result['age'] >= 21 else 'child'
address = result['address']
await db.insert_data(person, address)
return print(address, person)
except Exception as e:
print(e)
async def bound_fetch(sem, url, session):
async with sem:
return await check_address(url, session)
async def main():
await db.connect()
urls = [url for i in URLS]
sem = asyncio.Semaphore(50)
tasks = []
async with aiohttp.ClientSession() as session:
for url in urls:
task = asyncio.ensure_future(bound_fetch(sem, url, session))
tasks.append(task)
responses = asyncio.gather(*tasks)
await responses
if __name__ == '__main__':
asyncio.run(main())
awaiting for a task to complete is slow. By necessity await will wait for a task to complete. When it is not necessary to wait, simply carrying on and not blocking your code is generally preferable.
As you mention, this line: await db.insert_data(person, address) is slow. That's because it is awaiting the db.insert. But you are not using the result of this so rather than await you can just omit the await and allow the insert to go ahead asynchronously.
You mention asynchio, the python documentation gives a good overview of this here.
I'm writing a class that will do http requests using aiohttp. According to the docs I should not to create a ClientSession per request, so I want to reuse the same session.
code:
class TestApi:
def __init__(self):
self.session = aiohttp.ClientSession()
# async defs methods from here
When doing
TestApi()
I get the error: Unclosed client session.
What is the solution to persist the ClientSession object?
The expression TestApi() on a line by itself creates a TestApi object and immediately throws it away. aiohttp complaints that the session was never closed (either by leaving an async with block or with an explicit call to close()), but even without the warning it doesn't make sense not to assign the API object to a variable where it will be actually used.
To reuse the session, your code needs to have access to the session, or to an object that holds it:
async def fetch(url):
async with aiohttp.request('GET', url) as resp:
resp.raise_for_status()
return await resp.read()
async def main():
url1_data, url2_data = asyncio.gather(
fetch('http://url1'), fetch('http://url2'))
url3_data, url4_data = asyncio.gather(
fetch('http://url3'), fetch('http://url4'))
One option is to add a session parameter to fetch (and other functions) and consistently call it with a session created in main(). A better option is to create an API class and convert the global functions like fetch to methods:
class Http:
async def __aenter__(self):
self._session = aiohttp.ClientSession()
return self
async def __aexit__(self, *err):
await self._session.close()
self._session = None
async def fetch(self, url):
async with self._session.get(url) as resp:
resp.raise_for_status()
return await resp.read()
main() can still exist as a function, but it can consistently use the object that holds the session:
async def main():
async with Http() as http:
url1_data, url2_data = await asyncio.gather(
http.fetch('http://url1'), http.fetch('http://url2'))
url3_data, url4_data = await asyncio.gather(
http.fetch('http://url3'), http.fetch('http://url4'))
In the above code, the async with statement is used to ensure that the session is closed whenever the scope is left.
I try to reuse HTTP-session as aiohttp docs advice
Don’t create a session per request. Most likely you need a session per
application which performs all requests altogether.
But usual pattern which I use with requests lib doesn`t work:
def __init__(self):
self.session = aiohttp.ClientSession()
async def get_u(self, id):
async with self.session.get('url') as resp:
json_resp = await resp.json()
return json_resp.get('data', {})
Then I try to
await client.get_u(1)
I got error
RuntimeError: Timeout context manager should be used inside a task
Any workarounds with async_timeout didn't help.
Another way is working:
async def get_u(self, id):
async with aiohttp.ClientSession() as session:
with async_timeout.timeout(3):
async with session.get('url') as resp:
json_resp = await resp.json()
return json_resp.get('data', {})
But it seems like creating session per request.
So my question: how to properly reuse aiohttp-session?
UPD: minimal working example. Sanic application with following view
import aiohttp
from sanic.views import HTTPMethodView
class Client:
def __init__(self):
self.session = aiohttp.ClientSession()
self.url = 'https://jsonplaceholder.typicode.com/todos/1'
async def get(self):
async with self.session.get(self.url) as resp:
json_resp = await resp.json()
return json_resp
client = Client()
class ExView(HTTPMethodView):
async def get(self, request):
todo = await client.get()
print(todo)
I had the same error. The solution for me was initializing the client within an async function. EG:
class SearchClient(object):
def __init__(self, search_url: str, api_key: str):
self.search_url = search_url
self.api_key = api_key
self.session = None
async def _get(self, url, attempt=1):
if self.session is None:
self.session = aiohttp.ClientSession(raise_for_status=True)
headers = {
'Content-Type': 'application/json',
'api-key': self.api_key
}
logger.info("Running Search: {}".format(url))
try:
with timeout(60):
async with self.session.get(url, headers=headers) as response:
results = await response.json()
return results
For example you can create ClientSession on app start (using on_startup signal https://docs.aiohttp.org/en/stable/web_advanced.html#signals).
Store it to you app (aiohttp application has dict interface for such issues https://aiohttp.readthedocs.io/en/stable/faq.html#id4) and get access to your session through request.app['YOU_CLIENT_SESSION'] in request.