From b1d9a6a7de0c2500dc6a736cd3e442b3a32b5a38 Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Tue, 14 Oct 2025 15:15:23 +0200 Subject: [PATCH 1/2] Send logs to Matrix asynchronously --- app/core/utils/log.py | 7 +++++-- app/utils/communication/matrix.py | 18 ++++++++++++------ app/utils/loggers_tools/matrix_handler.py | 10 ++++++++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/core/utils/log.py b/app/core/utils/log.py index 8d6d517e74..02c5695201 100644 --- a/app/core/utils/log.py +++ b/app/core/utils/log.py @@ -7,6 +7,7 @@ from typing import Any import uvicorn +from fastapi import BackgroundTasks from app.core.utils.config import Settings @@ -78,7 +79,7 @@ class console_color: # Logging config # See https://docs.python.org/3/library/logging.config.html#logging-config-dictschema - def get_config_dict(self, settings: Settings): + def _get_config_dict(self, settings: Settings): # We can't use a dependency to access settings as this function is not an endpoint. The object must thus be passed as a parameter. # /!\ WARNING /!\ @@ -121,6 +122,7 @@ def get_config_dict(self, settings: Settings): # Send error to a Matrix server. If credentials are not set in settings, the handler will be disabled "formatter": "matrix", "class": "app.utils.loggers_tools.matrix_handler.MatrixHandler", + "background_tasks": BackgroundTasks, "room_id": settings.MATRIX_LOG_ERROR_ROOM_ID, "token": settings.MATRIX_TOKEN, "server_base_url": settings.MATRIX_SERVER_BASE_URL, @@ -133,6 +135,7 @@ def get_config_dict(self, settings: Settings): # Send error to a Matrix server. If credentials are not set in settings, the handler will be disabled "formatter": "matrix", "class": "app.utils.loggers_tools.matrix_handler.MatrixHandler", + "background_tasks": BackgroundTasks, "room_id": settings.MATRIX_LOG_AMAP_ROOM_ID, "token": settings.MATRIX_TOKEN, "server_base_url": settings.MATRIX_SERVER_BASE_URL, @@ -398,7 +401,7 @@ def initialize_loggers(self, settings: Settings): # If logs/ folder does not exist, the logging module won't be able to create file handlers Path("logs/").mkdir(parents=True, exist_ok=True) - config_dict = self.get_config_dict(settings=settings) + config_dict = self._get_config_dict(settings=settings) logging.config.dictConfig(config_dict) loggers = [logging.getLogger(name) for name in config_dict["loggers"]] diff --git a/app/utils/communication/matrix.py b/app/utils/communication/matrix.py index 566689fea8..c93232a34b 100644 --- a/app/utils/communication/matrix.py +++ b/app/utils/communication/matrix.py @@ -1,6 +1,6 @@ from typing import Any -import requests +import httpx from app.types.exceptions import MatrixRequestError, MatrixSendMessageError @@ -24,7 +24,7 @@ def __init__( self.access_token = token - def post( + async def post( self, url: str, json: dict[str, Any], @@ -43,15 +43,21 @@ def post( if "Authorization" not in headers: headers["Authorization"] = "Bearer " + self.access_token - response = requests.post(url, json=json, headers=headers, timeout=10) try: + async with httpx.AsyncClient() as client: + response = await client.post( + url, + json=json, + headers=headers, + timeout=10, + ) response.raise_for_status() - except requests.exceptions.HTTPError as err: + except httpx.RequestError as err: raise MatrixRequestError() from err return response.json() - def send_message(self, room_id: str, formatted_body: str) -> None: + async def send_message(self, room_id: str, formatted_body: str) -> None: """ Send a message to the room `room_id`. `formatted_body` can contain html formatted text @@ -71,6 +77,6 @@ def send_message(self, room_id: str, formatted_body: str) -> None: } try: - self.post(url, json=data, headers=None) + await self.post(url, json=data, headers=None) except MatrixRequestError as error: raise MatrixSendMessageError(room_id=room_id) from error diff --git a/app/utils/loggers_tools/matrix_handler.py b/app/utils/loggers_tools/matrix_handler.py index d09d0e6644..f89fffd97f 100644 --- a/app/utils/loggers_tools/matrix_handler.py +++ b/app/utils/loggers_tools/matrix_handler.py @@ -1,6 +1,7 @@ import logging from logging import StreamHandler +from fastapi import BackgroundTasks from typing_extensions import override from app.utils.communication.matrix import Matrix @@ -21,6 +22,7 @@ class MatrixHandler(StreamHandler): def __init__( self, + background_tasks: BackgroundTasks, room_id: str, token: str, server_base_url: str | None, @@ -30,6 +32,7 @@ def __init__( super().__init__() self.setLevel(level) + self.background_tasks = background_tasks self.room_id = room_id self.enabled = enabled if self.enabled: @@ -42,9 +45,12 @@ def __init__( def emit(self, record): if self.enabled: msg = self.format(record) - try: - self.matrix.send_message(self.room_id, msg) + self.background_tasks.add_task( + self.matrix.send_message, + room_id=self.room_id, + formatted_body=msg, + ) # We should catch and log any error, as Python may discarded them in production except Exception as err: # We use warning level so that the message is not sent to matrix again From 1f7a908ca3b7ec68ea1ca55d299a18fecb88019e Mon Sep 17 00:00:00 2001 From: Marc-Andrieu Date: Tue, 14 Oct 2025 15:18:49 +0200 Subject: [PATCH 2/2] Remove `requests` from direct dependencies, promote `httpx` to common dependencies --- requirements-common.txt | 2 +- requirements-dev.txt | 2 -- tests/test_payment.py | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/requirements-common.txt b/requirements-common.txt index 622213bbe8..d29c1bd509 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -12,6 +12,7 @@ fastapi[standard]==0.115.6 firebase-admin==6.5.0 # Firebase is used for push notification google-auth-oauthlib==1.2.1 helloasso-python==1.0.5 +httpx==0.27.0 icalendar==5.0.13 jellyfish==1.0.4 # String Matching Jinja2==3.1.6 # template engine for html files @@ -25,7 +26,6 @@ pypdf==4.3.1 python-dotenv==1.0.1 # load environment variables from .env file python-multipart==0.0.18 # a form data parser, as oauth flow requires form-data parameters redis==5.0.8 -requests==2.32.4 sqlalchemy-utils == 0.41.2 SQLAlchemy[asyncio]==2.0.32 # [asyncio] allows greenlet to be installed on Apple M1 devices. unidecode==1.3.8 diff --git a/requirements-dev.txt b/requirements-dev.txt index 552d20ede4..c21a3be872 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,6 @@ aiosqlite==0.20.0 boto3-stubs[essential]==1.38.23 google-auth-stubs==0.3.0 -httpx==0.27.0 # needed for tests as a replacement of requests in TestClient mypy[faster-cache]==1.16.0 pytest-alembic==0.12.1 pytest-asyncio==0.26.0 @@ -15,4 +14,3 @@ types-Authlib==1.5.0.20250516 types-fpdf2==2.8.3.20250516 types-psutil==7.0.0.20250601 types-redis==4.6.0.20241004 -types-requests==2.32.0.20250515 diff --git a/tests/test_payment.py b/tests/test_payment.py index 88cd84264a..33f9af6ebd 100644 --- a/tests/test_payment.py +++ b/tests/test_payment.py @@ -12,7 +12,6 @@ HelloAssoApiV5ModelsCartsInitCheckoutResponse, ) from pytest_mock import MockerFixture -from requests import Response from sqlalchemy.ext.asyncio import AsyncSession from app.core.payment import cruds_payment, models_payment, schemas_payment @@ -495,8 +494,6 @@ def init_a_checkout_side_effect( init_checkout_body: HelloAssoApiV5ModelsCartsInitCheckoutBody, ): if init_checkout_body.payer is not None: - r = Response() - r.status_code = 400 raise UnauthorizedException return HelloAssoApiV5ModelsCartsInitCheckoutResponse( id=7,