Related
I have the fast API application and run schedule task in a background thread as a startup event in fast API.
so when I use the SQlAlchemy async session in route scope like:
session: AsyncSession=Depends(instance_manger.db_instance.get_db_session) it's ok and runs as correct , but when it's run in the background thread I have the below error.
I use python module => SQLAlchemy[asyncio] asyncmy pymysql fastapi
database.py
class DBManager:
def __init__(self):
self.SQLALCHEMY_DATABASE_URL = None
self.config_reader_instance = None
self.engine = None
self._session_factory = None
self.logger_handler_instance = None
self.db = None
def initialize(self, config_reader_instance, logger_handler_instance):
self.logger_handler_instance = logger_handler_instance
self.config_reader_instance = config_reader_instance
self.SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://{0}:{1}#{2}:{3}/{4}".format(
self.config_reader_instance.DB_INFO['db_username'], self.config_reader_instance.DB_INFO['db_password'],
self.config_reader_instance.DB_INFO['db_hostname'], self.config_reader_instance.DB_INFO['db_port'],
self.config_reader_instance.DB_INFO['db_name'])
self.engine = create_async_engine(self.SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
# self.engine.begi/n()
self._session_factory = async_scoped_session(sessionmaker(
self.engine, class_=AsyncSession, expire_on_commit=False), scopefunc=current_task)
# self._session_factory = orm.scoped_session(
# orm.sessionmaker(
# class_=AsyncSession,
# autoflush=False,
# bind=self.engine,
# ),
# )
async def get_db_session(self) -> AsyncSession:
async with self._session_factory() as session:
try:
yield session
except Exception as e:
self.logger_handler_instance.write_log(__name__, logging.FATAL,
'Session rollback because of exception')
self.logger_handler_instance.write_log(__name__, logging.FATAL, e)
await session.rollback()
raise
finally:
await session.close()
background_thread.py
class BackgroundRunnable:
def __init__(self):
self.instance_manger = None
self.core_process_instance = None
self.conf_reader_instance = None
self.process_id = None
self.process_name = "BTC"
def initialize(self, instance_manager: InstanceManager):
self.instance_manger = instance_manager
return self
def set_process_info(self, process_name):
self.process_id = os.getpid()
self.process_name = process_name
async def run_main(self):
self.instance_manger.logger_handler_instance.write_log(__name__, logging.INFO,
"Background Thread is start")
results = await CryptoCoinService(
CryptoCoinRepository(AsyncSession(self.instance_manger.db_instance.engine))).get_coin()
print(results)
crypto_coin_repository.py
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import class_mapper
from db.models.models import CryptoCoinModel
class CryptoCoinRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def get_all(self) -> bool:
results = await self.session.execute(
select(CryptoCoinModel._id).where(CryptoCoinModel._symbol == 'BTC'))
results_ = results.fetchone()
if results_.__len__() == 0:
return False
else:
return True
main.py
from fastapi import APIRouter, Depends, Request, Response, FastAPI, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from coin_server.background_thread import BackgroundRunnable
from coin_server.core_process import CoreProcess
from core.instance_manager import InstanceManager
from db.database import DBManager
from db.repository.crypto_coin_repository import CryptoCoinRepository
from db.services.crypto_coin_service import CryptoCoinService
deposit_Router = APIRouter()
instance_manager = InstanceManager()
instance_manager.initialize()
db_instance = DBManager()
db_instance.initialize(instance_manager.config_reader_instance, instance_manager.logger_handler_instance)
#deposit_Router.post('/')
async def index(request: Request, session: AsyncSession = Depends(db_instance.get_db_session)):
results = await CryptoCoinService(CryptoCoinRepository(session)).get_coin()
print(results)
deposit_app = FastAPI()
#deposit_app.on_event('startup')
async def app_startup():
background_runnable = BackgroundRunnable()
background_runnable.initialize(instance_manager)
asyncio.create_task(background_runnable.run_main())
# asyncio.create_task(BackgroundRunnable().initialize(instance_manager).run_main())
deposit_app.include_router(deposit_Router)
when I run fast API app error like belong output.
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
INFO: Started reloader process [176] using watchgod
INFO: Started server process [179]
INFO: Waiting for application startup.
Task exception was never retrieved
future: <Task finished name='Task-3' coro=<BackgroundRunnable.run_main() done, defined at /mnt/c/Users/dr_r00t3r/Desktop/main/coin_server/background_thread.py:48> exception=At
tributeError("'async_generator' object has no attribute 'execute'")>
Traceback (most recent call last):
File "/mnt/c/Users/dr_r00t3r/Desktop/main/coin_server/background_thread.py", line 51, in run_main
results = await CryptoCoinService(
File "/mnt/c/Users/dr_r00t3r/Desktop/main/db/repository/crypto_coin_repository.py", line 17, in get_all
results = await self.session.execute(
AttributeError: 'async_generator' object has no attribute 'execute'
INFO: Application startup complete.
It's all note: when you use function get_db_session in database.py like a generator, close function of session doesn't work as auto, so you should close them like manually.
database.py
import logging
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
Base = declarative_base()
class DBManager:
def __init__(self):
self.SQLALCHEMY_DATABASE_URL = None
self.config_reader_instance = None
self.engine = None
self.session_factory = None
self.Base = declarative_base()
self.logger_handler_instance = None
def initialize(self, config_reader_instance, logger_handler_instance):
self.logger_handler_instance = logger_handler_instance
self.config_reader_instance = config_reader_instance
self.SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://{0}:{1}#{2}:{3}/{4}".format(
self.config_reader_instance.DB_INFO['db_username'], self.config_reader_instance.DB_INFO['db_password'],
self.config_reader_instance.DB_INFO['db_hostname'], self.config_reader_instance.DB_INFO['db_port'],
self.config_reader_instance.DB_INFO['db_name'])
self.engine = create_async_engine(self.SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, pool_size=30,
max_overflow=30, echo_pool=True, echo=False,
pool_recycle=3600) # recycle every hour
DBManager.Base = declarative_base()
self.session_factory = scoped_session(sessionmaker(
self.engine, class_=AsyncSession, expire_on_commit=False
))
def get_db_session(self):
session = self.session_factory()
try:
yield session
except Exception as e:
self.logger_handler_instance.log(__name__, logging.FATAL,
'Session rollback because of exception')
self.logger_handler_instance.log(__name__, logging.FATAL, e)
session.rollback()
raise
finally:
session.close()
async def init_models(self):
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
background_thread.py
class BackgroundRunnable:
def __init__(self):
self.instance_manger = None
self.core_process_instance = None
self.conf_reader_instance = None
self.process_id = None
self.process_name = "BTC"
def initialize(self, instance_manager: InstanceManager):
self.instance_manger = instance_manager
return self
def set_process_info(self, process_name):
self.process_id = os.getpid()
self.process_name = process_name
async def run_main(self):
self.instance_manger.logger_handler_instance.write_log(__name__, logging.INFO,
"Background Thread is start")
self.session: AsyncSession = next(self.instance_manger.db_instance.get_db_session())
results = await CryptoCoinService(CryptoCoinRepository(self.session)).get_coin(
self.instance_manger.config_reader_instance.BTC_INFO['BTC_COIN'])
print(results)
crypto_coin_repository.py
"""Repositories module."""
from contextlib import AbstractContextManager
from typing import Callable
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import class_mapper, Session
from db.models.models import CryptoCoinModel
class CryptoCoinRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def get_all(self, coin) -> bool:
results = await self.session.execute(
select(CryptoCoinModel._id).where(CryptoCoinModel._symbol == coin))
results = results.fetchall()
if len(results) == 0:
return False
else:
return True
def serialize(self, model):
"""Transforms a model into a dictionary which can be dumped to JSON."""
# first we get the names of all the columns on your model
columns = [c.key for c in class_mapper(model.__class__).columns]
# then we return their values in a dict
return dict((c, getattr(model, '_' + c)) for c in columns)
class NotFoundError(Exception):
symbol: str
def __init__(self):
super().__init__(f'{self._symobl} not found,please add this coin to db')
class CryptoCoinNotFoundError(NotFoundError):
# entity_name: str = 'User'
pass
main.py
from fastapi import APIRouter, Depends, Request, Response, FastAPI, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from coin_server.background_thread import BackgroundRunnable
from coin_server.core_process import CoreProcess
from core.instance_manager import InstanceManager
from db.database import DBManager
from db.repository.crypto_coin_repository import CryptoCoinRepository
from db.services.crypto_coin_service import CryptoCoinService
deposit_Router = APIRouter()
instance_manager = InstanceManager()
instance_manager.initialize()
db_instance = DBManager()
db_instance.initialize(instance_manager.config_reader_instance, instance_manager.logger_handler_instance)
#deposit_Router.post('/')
async def index(request: Request, session: AsyncSession = Depends(db_instance.get_db_session)):
results = await CryptoCoinService(CryptoCoinRepository(session)).get_coin()
print(results)
deposit_app = FastAPI()
#deposit_app.on_event('startup')
async def app_startup():
background_runnable = BackgroundRunnable()
background_runnable.initialize(instance_manager)
asyncio.create_task(background_runnable.run_main())
# asyncio.create_task(BackgroundRunnable().initialize(instance_manager).run_main())
deposit_app.include_router(deposit_Router)
I am writing an API using FastAPI when I run with uvicorn everything is normal, I get the error when I want to run a test using the FastAPI TestClient.
This is the error:
async def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = await collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
TypeError: object dict can't be used in 'await' expression
db\db.py:12: TypeError
project structure:
APP
|--__init__.py
|--run.py
|--main.py
|--test
|--test_app.py
|--routes
|--router.py
|--models
|--models.py
|--db
|--db_conn.py
|--db.py
|--auth_jwt
|--jwt_auth.py
|--auth
|--auth.py
This is the code of the test, I am using mongomock, I don't know if this will be the root of the problem:
import collections
from fastapi.testclient import TestClient
from fastapi import status
from main import app
from mongoengine import connect, disconnect, get_connection
from db.db_conn import db
client = TestClient(app)
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
db.client = get_connection('testdb')
db.client["CIA"]
db.client["Employees_Info"]
db.client.CIA.Employees_Info.insert_one({"name": "user_Name","password": "week"})
def test_ping():
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"message": "Conectado"}
def test_login():
data = {"username":'user_name', 'password':'week'}
response = client.post("/login", data=data)
assert response.headers['Content-Type'] == 'application/json'
assert response.status_code == status.HTTP_200_OK
db.client.disconnect()
I tried performing the Async test according to the FastAPI documentation but it doesn't work either, if I use the "normal" database the test works.
router.py
#router.post("/login", tags=["user"], response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncIOMotorClient = Depends(get_database)):
authenticate_user_id = await authenticate_user(db, form_data.username, form_data.password)
if not authenticate_user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"user_id": str(authenticate_user_id["_id"])})
return {"access_token": access_token, "token_type": "bearer"}
auth.py
async def authenticate_user(conn:AsyncIOMotorClient, username, password):
user_id = await verify_user(conn, username)
if not user_id:
return False
if not await verify_password(conn, user_id, password):
return False
return user_id
async def verify_user(conn, user):
return await get_user_id(conn,user)
async def verify_password(conn, user_id, password):
return pbkdf2_sha256.verify(password, await get_password(conn, user_id))
db.py
async def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = await collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
print(type(user))
if user:
return user
async def get_password(conn, user_id):
collection = conn.CIA.get_collection("Employees_Info")
db = await collection.find_one(user_id)
if db:
return db['password']
Maybe you need install pytest-asyncio.Here more info https://fastapi.tiangolo.com/advanced/async-tests/
collection.find_one() is not a async function, so you are trying to await the result of the function which is a dict, that is why you are getting the error TypeError: object dict can't be used in 'await' expression you are awaiting a dict, not a coroutine which would be returned by an async function.
To fix you code just remove await from
db = await collection.find_one(user_id)
When you do that, you won't really need it to be a async function, so you can just define it regularly, but you will than have to change all the function calls and remove the await from them, otherwise you will get this error again
Full code:
db.py
def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
print(type(user))
if user:
return user
def get_password(conn, user_id):
collection = conn.CIA.get_collection("Employees_Info")
db = collection.find_one(user_id)
if db:
return db['password']
auth.py
def authenticate_user(conn:AsyncIOMotorClient, username, password):
user_id = verify_user(conn, username)
if not user_id:
return False
if not verify_password(conn, user_id, password):
return False
return user_id
def verify_user(conn, user):
return get_user_id(conn,user)
def verify_password(conn, user_id, password):
return pbkdf2_sha256.verify(password, get_password(conn, user_id))
router.py
# This probably has to stay as an async function, I'm not sure how the module works
#router.post("/login", tags=["user"], response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncIOMotorClient = Depends(get_database)):
authenticate_user_id = authenticate_user(db, form_data.username, form_data.password)
if not authenticate_user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"user_id": str(authenticate_user_id["_id"])})
return {"access_token": access_token, "token_type": "bearer"}
import collections
from fastapi.testclient import TestClient
from fastapi import status
from main import app
from mongoengine import connect, disconnect, get_connection
from db.db_conn import db
client = TestClient(app)
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
db.client = get_connection('testdb')
db.client["CIA"]
db.client["Employees_Info"]
db.client.CIA.Employees_Info.insert_one({"name": "user_Name","password": "week"})
def test_ping():
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"message": "Conectado"}
def test_login():
data = {"username":'user_name', 'password':'week'}
response = client.post("/login", data=data)
assert response.headers['Content-Type'] == 'application/json'
assert response.status_code == status.HTTP_200_OK
db.client.disconnect()
My question is closely related to the following question on Stackoverflow and the documentation here.
I am defining a websockets-connection as a class. Next, I create a new class where I call the earlier defined websocket-class as self.ws and tell which data to send to the websocket with self.request.
My problem is that the current script only runs once, whereas my desired output is continuous data.
The second link shows that I can retrieve continuous / streaming data using
asyncio.get_event_loop().run_until_complete(call_api(json.dumps(msg)))
I include all of the above code in my code (call_api is defined differently due to the desire to write it as a class). Below is my code:
import sys, json
import asyncio
from websockets import connect
class EchoWebsocket:
def __init__(self, URL, CLIENT_ID=None, CLIENT_SECRET=None):
self.url = URL
self.client_id = CLIENT_ID
self.client_secret = CLIENT_SECRET
async def __aenter__(self):
self._conn = connect(self.url)
self.websocket = await self._conn.__aenter__()
return self
async def __aexit__(self, *args, **kwargs):
await self._conn.__aexit__(*args, **kwargs)
async def send(self, message):
await self.websocket.send(message)
async def receive(self):
return await self.websocket.recv()
class DERIBIT:
def __init__(self):
self.ws = EchoWebsocket(URL='wss://test.deribit.com/ws/api/v2')
self.loop = asyncio.get_event_loop()
self.request = \
{"jsonrpc": "2.0",
"method": "public/subscribe",
"id": 42,
"params": {
"channels": ["deribit_price_index.btc_usd"]}
}
def get_ticks(self):
return self.loop.run_until_complete(self.__async__get_ticks())
async def __async__get_ticks(self):
async with self.ws as echo:
await echo.send(json.dumps(self.request))
response = await echo.receive()
print(response)
if __name__ == "__main__":
deribit = DERIBIT()
deribit.get_ticks()
This script gives the following output:
{"jsonrpc": "2.0", "method": "public/subscribe", "id": 42, "params": {"channels": ["deribit_price_index.btc_usd"]}}
whereas I would like to see
Please advice.
I only worked with Tornado's websockets but they work pretty well and Tornado has many helpers for dealing with async code:
import json
import tornado
from tornado.ioloop import PeriodicCallback
from tornado.websocket import websocket_connect
class EchoWebsocket:
def __init__(self, url, client_id=None, client_secret=None):
self.url = url
self.client_id = client_id
self.client_secret = client_secret
self.websocket = None
async def connect(self):
if not self.websocket:
self.websocket = await websocket_connect(self.url)
async def close(self):
await self.websocket.close()
self.websocket = None
async def read(self):
return await self.websocket.read_message()
async def write(self, message):
await self.websocket.write_message(message)
class DERIBIT:
def __init__(self):
self.ws = EchoWebsocket(url='wss://test.deribit.com/ws/api/v2')
self.request = {
"jsonrpc": "2.0",
"method": "public/subscribe",
"id": 42,
"params": {
"channels": ["deribit_price_index.btc_usd"]}
}
self.callback = PeriodicCallback(self.get_ticks, 1000)
self.callback.start()
async def get_ticks(self):
if not self.ws.websocket:
await self.ws.connect()
await self.ws.write(json.dumps(self.request))
response = await self.ws.read()
print(response)
if __name__ == "__main__":
deribit = DERIBIT()
tornado.ioloop.IOLoop.current().start()
Output:
{"jsonrpc":"2.0","id":42,"result":["deribit_price_index.btc_usd"],"usIn":1587298852138977,"usOut":1587298852139023,"usDiff":46,"testnet":true}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587298851526,"price":7173.46,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587298852533,"price":7173.53,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","id":42,"result":["deribit_price_index.btc_usd"],"usIn":1587298852932540,"usOut":1587298852932580,"usDiff":40,"testnet":true}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587298852533,"price":7173.53,"index_name":"btc_usd"}}}
The example above could be simplified a lot if you integrate the websocket into the DERIBIT class rather than create a separate class for it.
the problem is in the function
first loop.run_until_complete run until the future is complete doc run_until_complete
that mean your function receive will run only one response. run_until_complete is not a callback function!.
so in your case the main:
deribit.get_ticks() -> run the future instance __async__get_ticks
so __async__get_ticks is task: let's see what the task do:
1.open ws connection:
2.send request
3.wait the response of the ws
4. print(response)
here the task is done that why you see only one line
async def __async__get_ticks(self):
async with self.ws as echo:
await echo.send(json.dumps(self.request))
response = await echo.receive()
print(response)
after explanation: the solution will be simple:
need to wrap the line response
with while
async def __async__get_ticks(self):
async with self.ws as echo:
await echo.send(json.dumps(self.request))
while True:
response = await echo.receive()
print(response)
output
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654476817,"price":7540.54,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654477824,"price":7540.52,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654478831,"price":7540.15,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654479838,"price":7539.83,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654480845,"price":7539.2,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654481852,"price":7538.96,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654482859,"price":7538.9,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654483866,"price":7538.89,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654484873,"price":7538.47,"index_name":"btc_usd"}}}
{"jsonrpc":"2.0","method":"subscription","params":{"channel":"deribit_price_index.btc_usd","data":{"timestamp":1587654485880,"price":7537.15,"index_name":"btc_usd"}}}
What's wrong with implementation of passing class instance as a dependency in FastAPI router or is it a bug?
1) I have defined router with dependency
app = FastAPI()
dbconnector_is = AsyncDBPool(conn=is_cnx, loop=None)
app.include_router(test_route.router, dependencies=[Depends(dbconnector_is)])
#app.on_event('startup')
async def startup():
app.logger = await AsyncLogger().getlogger(log)
await app.logger.warning('Webservice is starting up...')
await app.logger.info("Establishing RDBS Integration Pool Connection...")
await dbconnector_is.connect()
#app.on_event('startup')
async def startup():
await app.logger.getlogger(log)
await app.logger.warning('Webservice is starting up...')
await dbconnector_is.connect()
router
#router.get('/test')
async def test():
data = await dbconnector_is.callproc('is_processes_get', rows=-1, values=[None, None])
return Response(json.dumps(data, default=str))
custom class for passing it's instance as callable.
class AsyncDBPool:
def __init__(self, conn: dict, loop: None):
self.conn = conn
self.pool = None
self.connected = False
self.loop = loop
def __call__(self):
return self
async def connect(self):
while self.pool is None:
try:
self.pool = await aiomysql.create_pool(**self.conn, loop=self.loop, autocommit=True)
except aiomysql.OperationalError as e:
await asyncio.sleep(1)
continue
else:
return self.pool
And when I send request I receive this error.
data = await dbconnector_is.callproc('is_processes_get', rows=-1, values=[None, None])
NameError: name 'dbconnector_is' is not defined
I want to mock the json() coroutine from the aiohttp.ClientSession.get method. It looks to return an async generator object, which is where I'm confused on how to mock in my example. Here is my code:
async def get_access_token():
async with aiohttp.ClientSession(auth=auth_credentials) as client:
async with client.get(auth_path, params={'grant_type': 'client_credentials'}) as auth_response:
assert auth_response.status == 200
auth_json = await auth_response.json()
return auth_json['access_token']
This is my test case to mock the get method:
json_data = [{
'access_token': 'HSG9hsf328bJSWO82sl',
'expires_in': 86399,
'token_type': 'bearer'
}]
class AsyncMock:
async def __aenter__(self):
return self
async def __aexit__(self, *error_info):
return self
#pytest.mark.asyncio
async def test_wow_api_invalid_credentials(monkeypatch, mocker):
def mock_client_get(self, auth_path, params):
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = mocker.MagicMock(return_value=json_data)
return mock_response
monkeypatch.setattr('wow.aiohttp.ClientSession.get', mock_client_get)
result = await wow.get_access_token()
assert result == 'HSG9hsf328bJSWO82sl'
I think the problem might be that mock_response.json() is not awaitable. In my example I can't call await from a non async function so I'm confused on how I would do that. I would like to keep the test libraries to a minimum which is pytest and pytest-asyncio for the learning experiencing and to rely less on 3rd party libraries.
I was making it more complicated than it needed to be. I simply defined json as an awaitable attribute of AsyncMock which returns the json_data. The complete code looks like this:
json_data = {
'access_token': 'HSG9hsf328bJSWO82sl',
'expires_in': 86399,
'token_type': 'bearer'
}
class AsyncMock:
async def __aenter__(self):
return self
async def __aexit__(self, *error_info):
return self
async def json(self):
return json_data
#pytest.mark.asyncio
async def test_wow_api_invalid_credentials(monkeypatch):
def mock_client_get(self, auth_path, params):
mock_response = AsyncMock()
mock_response.status = 200
return mock_response
monkeypatch.setattr('wow.aiohttp.ClientSession.get', mock_client_get)
result = await wow.get_access_token()
assert result == 'HSG9hsf328bJSWO82sl'
This is part 1, but i suggest you watch part2.
Im not sure i understand your question totally, because using async def or #asyncio.coroutine can help you do this. Actually, i want to write it as comment, however there are so many differences that i can't put it into comment.
import asyncio
json_ = [{
'access_token': 'HSG9hsf328bJSWO82sl',
'expires_in': 86399,
'token_type': 'bearer'
}]
async def response_from_sun():
return json_
class AsyncMock:
async def specify(self):
return self.json[0].get("access_token")
async def __aenter__(self):
return self
async def __aexit__(self, *error_info):
return self
async def mock_client_get():
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = await response_from_sun()
return mock_response
async def go():
resp = await mock_client_get()
result = await resp.specify()
assert result == 'HSG9hsf328bJSWO82sl'
asyncio.get_event_loop().run_until_complete(go())
PART2
After adding my answer, i found there is a problem about your mock_response content. Becausemock_response does not contain variable and function which ClientResponse have.
Edit: I try many times and watch ClientSession's code, then i found you can specify a new response class by its parameter. Note: connector=aiohttp.TCPConnector(verify_ssl=False) is unnecessary
import asyncio
import aiohttp
class Mock(aiohttp.ClientResponse):
print("Mock")
async def specify(self):
json_ = (await self.json()).get("hello")
return json_
async def go():
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False),response_class=Mock) as session:
resp = await session.get("https://www.mocky.io/v2/5185415ba171ea3a00704eed")
result = await resp.specify()
print(result)
assert result == 'world'
asyncio.get_event_loop().run_until_complete(go())