diff --git a/santander_sdk/api_client/client.py b/santander_sdk/api_client/client.py index 289930d..c5d17bb 100644 --- a/santander_sdk/api_client/client.py +++ b/santander_sdk/api_client/client.py @@ -13,13 +13,11 @@ SantanderClientError, SantanderRequestError, ) -from .helpers import get_status_code_description, try_parse_response_to_json +from .helpers import try_parse_response_to_json BEFORE_EXPIRE_TOKEN_SECONDS = timedelta(seconds=60) TOKEN_ENDPOINT = "/auth/oauth/v2/token" -logger = logging.getLogger(__name__) - class SantanderApiClient: """ @@ -47,7 +45,7 @@ def __init__(self, config: SantanderClientConfiguration): self.session = BaseURLSession(base_url=config.base_url) self.session.cert = config.cert self.session.auth = SantanderAuth.from_config(config) - + self.logger = config.logger or logging.getLogger(__name__) self._set_default_workspace_id() def _set_default_workspace_id(self): @@ -58,7 +56,9 @@ def _set_default_workspace_id(self): "Conta sem configuração de workspace na configuração e na conta." ) - logger.info(f"Workspace obtido e configurado com sucesso: {workspace_id}") + self.logger.info( + f"Workspace obtido e configurado com sucesso: {workspace_id}" + ) self.config.set_workspace_id(workspace_id) def get(self, endpoint: str, params: dict | None = None) -> dict: @@ -94,17 +94,81 @@ def _request( params: dict | None = None, ) -> dict: url = self._prepare_url(endpoint) + response = None try: response = self.session.request( method, url, json=data, params=params, timeout=60 ) response.raise_for_status() + self._log_request_success_if_needed(method, url, params, data, response) + return response.json() except requests.exceptions.RequestException as e: status_code = getattr(e.response, "status_code", 0) error_content = try_parse_response_to_json(e.response) - status_description = get_status_code_description(status_code) + self._log_error_if_needed(method, url, params, data, e) + raise SantanderRequestError( + "Not successful code", status_code, error_content + ) - raise SantanderRequestError(status_description, status_code, error_content) except Exception as e: - raise SantanderRequestError(f"Erro na requisição: {e}", 0, None) from e + self._log_error_if_needed(method, url, params, data, e) + raise SantanderRequestError("Error in request: %s" % str(e), 0, None) from e + + def _log_error_if_needed( + self, + method: str, + url: str, + params: dict | None, + data: dict | None, + error: Exception | None, + ): + if self.config.log_request_response_level not in ["ALL", "ERROR"]: + self.logger.info("Logging error is disabled in client configuration") + + response = getattr(error, "response", None) + extra = self._get_request_summary( + method, url, response, request_data=data, request_params=params, error=error + ) + self.logger.error("API request failed", extra=extra) + + def _log_request_success_if_needed( + self, + method: str, + url: str, + params: dict | None, + data: dict | None, + response: requests.Response, + ): + if not self.config.log_request_response_level == "ALL": + self.logger.info("Request successful", url) + return + + extra = self._get_request_summary( + method, url, response, request_data=data, request_params=params + ) + self.logger.info("API request successful", extra=extra) + + def _get_request_summary( + self, + method: str, + url: str, + response: requests.Response | None, + request_data: dict | None = None, + request_params: dict | None = None, + error: Exception | None = None, + ) -> dict: + return { + "method": method, + "url": url, + "request_body": request_data, + "request_params": request_params, + "status_code": response.status_code if response is not None else None, + "response_body": try_parse_response_to_json(response) + if response is not None + else None, + "status": "error" if error else "success", + "error": {"message": str(error), "type": type(error).__name__} + if error + else None, + } diff --git a/santander_sdk/api_client/client_configuration.py b/santander_sdk/api_client/client_configuration.py index bec71f5..7849859 100644 --- a/santander_sdk/api_client/client_configuration.py +++ b/santander_sdk/api_client/client_configuration.py @@ -1,3 +1,7 @@ +from typing import Literal +import logging + + class SantanderClientConfiguration: def __init__( self, @@ -6,12 +10,16 @@ def __init__( cert: str, base_url: str, workspace_id: str = "", + log_request_response_level: Literal["ERROR", "ALL", "NONE"] = "ERROR", + logger: logging.Logger | None = None, ): self.client_id = client_id self.client_secret = client_secret self.workspace_id = workspace_id self.cert = cert self.base_url = base_url + self.log_request_response_level = log_request_response_level + self.logger = logger or logging.getLogger(__name__) def set_workspace_id(self, workspace_id: str): self.workspace_id = workspace_id diff --git a/santander_sdk/api_client/helpers.py b/santander_sdk/api_client/helpers.py index 75d55ac..8442c8c 100644 --- a/santander_sdk/api_client/helpers.py +++ b/santander_sdk/api_client/helpers.py @@ -61,26 +61,6 @@ def try_parse_response_to_json(response) -> dict | None: return error_content -SANTANDER_STATUS_DESCRIPTIONS = { - 200: "Sucesso", - 201: "Recurso criado", - 400: "Erro de informação do cliente", - 401: "Não autorizado/Autenticado", - 403: "Não Autorizado", - 404: "Informação não encontrada", - 406: "O recurso de destino não possui uma representação atual que seria aceitável", - 422: "Entidade não processa/inadequada", - 429: "O usuário enviou muitas solicitações em um determinado período", - 500: "Erro de Servidor, Aplicação está fora", - 501: "O servidor não oferece suporte à funcionalidade necessária para atender à solicitação", -} - - -def get_status_code_description(status_code: int | str) -> str: - """Retorna a descrição do status do Santander""" - return f"{status_code} - {SANTANDER_STATUS_DESCRIPTIONS.get(int(status_code), 'Erro desconhecido')}" - - def only_numbers(s): return re.sub("[^0-9]", "", s) if s else s diff --git a/santander_sdk/payment_receipts.py b/santander_sdk/payment_receipts.py index 43ed8ad..0982a80 100644 --- a/santander_sdk/payment_receipts.py +++ b/santander_sdk/payment_receipts.py @@ -42,7 +42,6 @@ https://developer.santander.com.br/api/documentacao/comprovantes-visao-geral/ """ -import logging from time import sleep from typing import Generator, List, cast from santander_sdk.api_client.client import SantanderApiClient @@ -60,8 +59,6 @@ RECEIPTS_ENDPOINT = "/consult_payment_receipts/v1/payment_receipts" -logger = logging.getLogger(__name__) - def payment_list( client: SantanderApiClient, params: ListPaymentParams @@ -161,10 +158,12 @@ def _handle_already_created( This happens when a receipt was requested a long time ago or the previous attempt returned an error like EXPUNGED or ERROR. """ - logger.info("Receipt already requested. Trying to get the receipt request ID.") + client.logger.info( + "Receipt already requested. Trying to get the receipt request ID." + ) receipt_history = receipt_creation_history(client, payment_id) if not receipt_history["paymentReceiptsFileRequests"]: - logger.error("No previous receipts in history") + client.logger.error("No previous receipts in history") raise last_from_history = receipt_history["paymentReceiptsFileRequests"][-1] request_id = last_from_history["request"]["requestId"] @@ -172,7 +171,7 @@ def _handle_already_created( if result["status"] not in [ReceiptStatus.EXPUNGED, ReceiptStatus.ERROR]: return result - logger.info("The last receipt is in an error state, creating another one.") + client.logger.info("The last receipt is in an error state, creating another one.") sleep(0.5) endpoint = f"{RECEIPTS_ENDPOINT}/{payment_id}/file_requests" response = cast(ReceiptInfoResponse, client.post(endpoint, None)) diff --git a/santander_sdk/pix.py b/santander_sdk/pix.py index 89b3630..d413e2b 100644 --- a/santander_sdk/pix.py +++ b/santander_sdk/pix.py @@ -1,5 +1,4 @@ from decimal import Decimal as D -import logging import uuid from typing import cast @@ -17,7 +16,6 @@ TransferPixResult, ) -logger = logging.getLogger("santanderLogger") PIX_ENDPOINT = "/management_payments_partners/v1/workspaces/:workspaceid/pix_payments" @@ -29,7 +27,7 @@ def transfer_pix( tags: list[str] = [], id: uuid.UUID | str | None = None, ) -> TransferPixResult: - transfer_flow = SantanderPaymentFlow(client, PIX_ENDPOINT, logger) + transfer_flow = SantanderPaymentFlow(client, PIX_ENDPOINT) try: if value is None or value <= 0: @@ -60,7 +58,7 @@ def transfer_pix( } except Exception as e: error_message = str(e) - logger.error(error_message) + client.logger.error(error_message) return { "success": False, "request_id": transfer_flow.request_id, @@ -83,7 +81,7 @@ def _generate_create_pix_dict( value: D, description: str, tags: list = [], - id: uuid.UUID | None = None, + id: uuid.UUID | str | None = None, ) -> dict: data = { "tags": tags, diff --git a/santander_sdk/transfer_flow.py b/santander_sdk/transfer_flow.py index be2a52a..91d9cd6 100644 --- a/santander_sdk/transfer_flow.py +++ b/santander_sdk/transfer_flow.py @@ -1,4 +1,3 @@ -import logging from time import sleep from typing import List, Literal, cast @@ -32,10 +31,8 @@ def __init__( self, client: SantanderApiClient, endpoint: str, - logger: logging.Logger | None = None, ): self.client = client - self.logger = logger or logging.getLogger(__name__) self.endpoint = endpoint self.request_id = None @@ -45,13 +42,13 @@ def create_payment(self, data: dict) -> SantanderPixResponse: ) self.request_id = response.get("id") self._check_for_rejected_error(response) - self.logger.info("Payment created: ", response.get("id")) + self.client.logger.info("Payment created: ", response.get("id")) return response def ensure_ready_to_pay(self, confirm_data) -> None: payment_status = confirm_data.get("status") if payment_status != CreateOrderStatus.READY_TO_PAY: - self.logger.info("PIX is not ready for payment", payment_status) + self.client.logger.info("PIX is not ready for payment", payment_status) self._payment_status_polling( payment_id=confirm_data.get("id"), until_status=[CreateOrderStatus.READY_TO_PAY], @@ -64,7 +61,7 @@ def confirm_payment( try: confirm_response = self._request_confirm_payment(confirm_data, payment_id) except SantanderRequestError as e: - self.logger.error(str(e), payment_id, "checking current status") + self.client.logger.error(str(e), payment_id, "checking current status") confirm_response = self._request_payment_status(payment_id) if not confirm_response.get("status") == ConfirmOrderStatus.PAYED: @@ -73,7 +70,9 @@ def confirm_payment( payment_id, confirm_response.get("status", "") ) except SantanderStatusTimeoutError as e: - self.logger.info("Timeout occurred while updating status:", str(e)) + self.client.logger.info( + "Timeout occurred while updating status:", str(e) + ) return confirm_response @retry_one_time_on_request_exception @@ -125,7 +124,7 @@ def _payment_status_polling( for attempt in range(1, max_update_attemps + 1): response = self._request_payment_status(payment_id) - self.logger.info( + self.client.logger.info( f"Checking status by polling: {payment_id} - {response.get('status')}" ) if response.get("status") in until_status: diff --git a/tests/test_client_unit.py b/tests/test_client_unit.py index f798bf6..d513c20 100644 --- a/tests/test_client_unit.py +++ b/tests/test_client_unit.py @@ -1,7 +1,10 @@ +import re import pytest from decimal import Decimal as D from unittest.mock import patch +import responses + from mock.santander_mocker import ( SANTANDER_URL, get_dict_payment_pix_request, @@ -12,10 +15,14 @@ from santander_sdk.api_client.client_configuration import ( SantanderClientConfiguration, ) -from santander_sdk.api_client.exceptions import SantanderClientError +from santander_sdk.api_client.exceptions import ( + SantanderClientError, + SantanderRequestError, +) from santander_sdk.types import OrderStatus +@responses.activate @pytest.fixture def client(): config = SantanderClientConfiguration( @@ -25,7 +32,18 @@ def client(): workspace_id="test_workspace_id", base_url=SANTANDER_URL, ) - return SantanderApiClient(config) + responses.add( + responses.POST, + re.compile(r".*v2/token$"), + json={"access_token": "test_access_token", "expires_in": 3600}, + status=200, + ) + client_instance = SantanderApiClient(config) + + with patch("logging.getLogger") as mock_get_logger: + mock_logger = mock_get_logger.return_value + client_instance.logger = mock_logger + yield client_instance @patch("santander_sdk.api_client.client.requests.Session.request") @@ -100,3 +118,73 @@ def test_patch_method(mock_request, client): mock_request.assert_called_once_with( "PATCH", "test_endpoint", data={"patch_data_key": "patch_data_value"} ) + + +@responses.activate +def test_request_log_success_all(client): + client.config.log_request_response_level = "ALL" + responses.add( + responses.POST, + re.compile(r".*/test_endpoint$"), + json={"id": "123456789", "status": "success"}, + status=200, + ) + + with patch.object(client, "logger") as mock_logger: + result = client._request( + "POST", "/test_endpoint", data={"pix_key": "123456789", "amount": "100.00"} + ) + + extra = mock_logger.info.call_args[1]["extra"] + assert result == {"id": "123456789", "status": "success"} + assert mock_logger.info.call_args[0][0] == "API request successful" + mock_logger.info.assert_called_once() + expected_extra = { + "method": "POST", + "url": "/test_endpoint", + "status_code": 200, + "request_body": {"pix_key": "123456789", "amount": "100.00"}, + "response_body": {"id": "123456789", "status": "success"}, + "status": "success", + } + + for key, value in expected_extra.items(): + assert extra[key] == value + + +@responses.activate +def test_request_log_error(client): + client.config.log_request_response_level = "ERROR" + request_data = {"pix_key": "123456789", "amount": "100.00"} + error_response = {"error": "Bad Request"} + + responses.add( + responses.POST, + f"{SANTANDER_URL}/test_endpoint", + json=error_response, + status=400, + ) + + with patch.object(client, "logger") as mock_logger: + with pytest.raises(SantanderRequestError): + client._request("POST", "/test_endpoint", data=request_data) + + mock_logger.error.assert_called_once() + assert mock_logger.error.call_args[0][0] == "API request failed" + + extra = mock_logger.error.call_args[1]["extra"] + expected_fields = { + "method": "POST", + "url": "/test_endpoint", + "status_code": 400, + "request_body": request_data, + "response_body": error_response, + "status": "error", + "error": { + "message": "400 Client Error: Bad Request for url: https://trust-sandbox.api.santander.com.br/test_endpoint", + "type": "HTTPError", + }, + } + + for key, value in expected_fields.items(): + assert extra[key] == value diff --git a/tests/test_helpers_unit.py b/tests/test_helpers_unit.py index a8c2973..615d6b5 100644 --- a/tests/test_helpers_unit.py +++ b/tests/test_helpers_unit.py @@ -4,7 +4,6 @@ from santander_sdk.api_client.helpers import ( download_file, - get_status_code_description, polling_until_condition, retry_one_time_on_request_exception, save_bytes_to_file, @@ -38,11 +37,6 @@ def test_get_pix_key_type_invalid(): get_pix_key_type("55 34 12345678") -def test_get_status_code_description(): - assert get_status_code_description(200) == "200 - Sucesso" - assert get_status_code_description(392) == "392 - Erro desconhecido" - - @pytest.fixture def mock_sleep_time(): with ( diff --git a/tests/test_transfer_flow_unit.py b/tests/test_transfer_flow_unit.py index 59ae57b..b5088ce 100644 --- a/tests/test_transfer_flow_unit.py +++ b/tests/test_transfer_flow_unit.py @@ -20,8 +20,7 @@ def api_client(): @pytest.fixture def payment_flow(api_client): - logger = MagicMock() - return SantanderPaymentFlow(api_client, PIX_ENDPOINT, logger) + return SantanderPaymentFlow(api_client, PIX_ENDPOINT) @pytest.fixture