├── src ├── tests │ ├── __init__.py │ └── test_fastAPI_aiohttp.py └── fastAPI_aiohttp │ ├── __init__.py │ └── fastAPI.py ├── requirements.txt ├── mypy.ini ├── .gitignore ├── README.md └── .github └── workflows └── python.yaml /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fastAPI_aiohttp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-asyncio 3 | aioresponses 4 | aiohttp 5 | fastapi 6 | httpx -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | strict = True 4 | files = src/fastAPI_aiohttp 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .pytest_cache 7 | # Temporary files 8 | .tmp 9 | 10 | 11 | # IDE 12 | .idea 13 | *iml 14 | venv* 15 | /env/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python](https://github.com/raphaelauv/fastAPI-aiohttp-example/workflows/Python/badge.svg?branch=master) 2 | 3 | # Full example of [FastAPI](https://github.com/tiangolo/fastapi) with an [aiohttp](https://github.com/aio-libs/aiohttp) client 4 | 5 | ### This is an example with FastAPI, but you can use this logic with any async ( ASGI ) web framework 6 | 7 | 8 | ### [EXAMPLE FOR HTTPX](https://github.com/raphaelauv/fastAPI-httpx-example/) 9 | 10 | 11 | #### Implemented logic : 12 | 13 | (with a fake server mocking answer of aiohttp) 14 | 15 | - Open ClientSession at fastAPI startup 16 | 17 | - Close ClientSession at fastAPI shutdown 18 | 19 | 20 | ## Tests 21 | - fastAPI endpoint test 22 | - aiohttp test 23 | -------------------------------------------------------------------------------- /.github/workflows/python.yaml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10" ,"3.11", "3.12", "3.13.0-rc.1"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install -r requirements.txt 23 | - name: Test with pytest 24 | run: | 25 | pip install pytest 26 | pytest 27 | -------------------------------------------------------------------------------- /src/tests/test_fastAPI_aiohttp.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import httpx 4 | from aioresponses import aioresponses 5 | from fastapi.testclient import TestClient 6 | import pytest 7 | 8 | from fastAPI_aiohttp.fastAPI import app, SingletonAiohttp 9 | 10 | 11 | @pytest.fixture 12 | def client_aio(): 13 | with aioresponses() as m: 14 | m.post(url="test/toto", 15 | status=200, 16 | body=json.dumps({"result": 2})) 17 | yield m 18 | 19 | 20 | @pytest.fixture 21 | def client_fastAPI(): 22 | return TestClient(app=app) 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_query_url(client_aio): 27 | rst = await SingletonAiohttp.query_url("test/toto") 28 | assert rst == {"result": 2} 29 | 30 | 31 | def test_endpoint(client_fastAPI): 32 | url = "http://localhost:8080/test" 33 | with aioresponses() as mock_server: 34 | mock_server.post(url=url, status=200, body=json.dumps({"success": 1})) 35 | 36 | result: httpx.Response = client_fastAPI.get(url='/endpoint/') 37 | assert result is not None 38 | 39 | result_json = result.json() 40 | assert result_json == {'success': 1} 41 | 42 | 43 | def test_endpoint_multi(client_fastAPI): 44 | url = "http://localhost:8080/test" 45 | with aioresponses() as mock_server: 46 | mock_server.post(url=url, status=200, body=json.dumps({"success": 1})) 47 | mock_server.post(url=url, status=200, body=json.dumps({"success": 2})) 48 | 49 | result: httpx.Response = client_fastAPI.get(url='/endpoint_multi/') 50 | assert result is not None 51 | 52 | result_json = result.json() 53 | assert result_json == {'success': 3} 54 | 55 | 56 | def test_endpoint_stream(client_fastAPI): 57 | data = b'TOTO' * 10000 58 | 59 | with client_fastAPI.stream('POST', url='/endpoint_stream/', content=data) as result: 60 | assert result is not None 61 | result.read() 62 | rst = result.content 63 | assert rst == b'RST' + data 64 | -------------------------------------------------------------------------------- /src/fastAPI_aiohttp/fastAPI.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import Coroutine 3 | from socket import AF_INET 4 | from typing import List, Optional, Any, Dict 5 | 6 | import aiohttp 7 | from fastapi import FastAPI 8 | from fastapi.logger import logger as fastAPI_logger # convenient name 9 | from fastapi.requests import Request 10 | from fastapi.responses import Response 11 | 12 | SIZE_POOL_AIOHTTP = 100 13 | 14 | 15 | class SingletonAiohttp: 16 | aiohttp_client: Optional[aiohttp.ClientSession] = None 17 | 18 | @classmethod 19 | def get_aiohttp_client(cls) -> aiohttp.ClientSession: 20 | if cls.aiohttp_client is None: 21 | timeout = aiohttp.ClientTimeout(total=2) 22 | connector = aiohttp.TCPConnector(family=AF_INET, limit_per_host=SIZE_POOL_AIOHTTP) 23 | cls.aiohttp_client = aiohttp.ClientSession(timeout=timeout, connector=connector) 24 | 25 | return cls.aiohttp_client 26 | 27 | @classmethod 28 | async def close_aiohttp_client(cls) -> None: 29 | if cls.aiohttp_client: 30 | await cls.aiohttp_client.close() 31 | cls.aiohttp_client = None 32 | 33 | @classmethod 34 | async def query_url(cls, url: str) -> Any: 35 | client = cls.get_aiohttp_client() 36 | 37 | try: 38 | async with client.post(url) as response: 39 | if response.status != 200: 40 | return {"ERROR OCCURED" + str(await response.text())} 41 | 42 | json_result = await response.json() 43 | except Exception as e: 44 | return {"ERROR": e} 45 | 46 | return json_result 47 | 48 | 49 | async def on_start_up() -> None: 50 | fastAPI_logger.info("on_start_up") 51 | SingletonAiohttp.get_aiohttp_client() 52 | 53 | 54 | async def on_shutdown() -> None: 55 | fastAPI_logger.info("on_shutdown") 56 | await SingletonAiohttp.close_aiohttp_client() 57 | 58 | 59 | app = FastAPI(docs_url="/", on_startup=[on_start_up], on_shutdown=[on_shutdown]) 60 | 61 | 62 | @app.get('/endpoint') 63 | async def endpoint() -> Any: 64 | url = "http://localhost:8080/test" 65 | return await SingletonAiohttp.query_url(url) 66 | 67 | 68 | @app.get('/endpoint_multi') 69 | async def endpoint_multi() -> Dict[str, int]: 70 | url = "http://localhost:8080/test" 71 | 72 | async_calls: List[Coroutine[Any, Any, Any]] = list() # store all async operations 73 | 74 | async_calls.append(SingletonAiohttp.query_url(url)) 75 | async_calls.append(SingletonAiohttp.query_url(url)) 76 | 77 | all_results: List[Dict[Any, Any]] = await asyncio.gather(*async_calls) # wait for all async operations 78 | return {'success': sum([x['success'] for x in all_results])} 79 | 80 | 81 | @app.post("/endpoint_stream/") 82 | async def endpoint_stream(request: Request): 83 | body = b'RST' 84 | async for chunk in request.stream(): 85 | body += chunk 86 | 87 | return Response(body, media_type='text/plain') 88 | 89 | 90 | if __name__ == '__main__': # local dev 91 | import uvicorn 92 | uvicorn.run(app, host="0.0.0.0", port=8000) 93 | --------------------------------------------------------------------------------