Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
"""Client API module."""

from typing import Any, Literal

import requests
from requests import HTTPError, Response, Session
from requests.auth import AuthBase
from sincpro_framework import DataTransferObject, logger

from sincpro_payments_sdk.exceptions import SincproExternalServiceError, SincproHTTPError


class ApiResponse(DataTransferObject):
    """API response model."""

    raw_response: dict | None = None


class ClientAPI:
    """Client API class."""

    def __init__(self, session: Session | None = None, auth: AuthBase | None = None) -> None:
        """Initialize the client API."""
        self._session = session
        self._auth = auth

    @property
    def use_session(self) -> bool:
        """Set the session."""
        return bool(self._session)

    @property
    def base_url(self) -> str:
        """Get the base URL for the API."""
        raise NotImplementedError("You must implement the base_url property")

    def execute_request(
        self,
        resource: str,
        method: Literal["GET", "POST", "PUT", "DELETE"] = "GET",
        params=None,
        headers=None,
        data=None,
        timeout=None,
        auth=None,
    ) -> Response:  # pylint: disable=too-many-arguments
        """Low-level function for API calls.
        It wraps the requests library for managing sessions and custom Exceptions

        Args:
            resource (str): Path of the resource.
            method (str, optional): HTTP method to execute. Defaults to "GET".
            params (Dict[str, Any], optional): Query parameters to include in the URL. Defaults to None.
            headers (Dict[str, str], optional): HTTP method to execute. Defaults to None.
            data (Any, optional): Payload to send in the body. Defaults to None.
            timeout (int, optional): Amount of seconds to wait for a timeout and raise an exception
            auth (AuthBase, optional): Authentication object to use in the request. Defaults to None.

        Raises:
            SincproHTTPError: HTTP error (4xx/5xx) from the external service.
            SincproExternalServiceError: Network-level error (connection, timeout, redirects).

        Returns:
            Response: Model for the HTTP response in requests
        """
        url = f"{self.base_url}{resource}"
        logger.debug(f"Requesting:[{method}] {url}", with_auth=bool(auth or self._auth))
        kwargs: dict[str, Any] = {
            "url": url,
            "method": method,
        }
        if data:
            kwargs["json"] = data
        if headers:
            kwargs["headers"] = headers
        if params:
            kwargs["params"] = params
            logger.debug(f"Params: {params}")
        if timeout:
            kwargs["timeout"] = timeout
        if self._auth:
            kwargs["auth"] = self._auth
        if auth:
            kwargs["auth"] = auth

        if method != "GET":
            logger.debug(f"Body: {data}")

        try:
            if self.use_session:
                assert self._session is not None
                response = self._session.request(**kwargs)
            else:
                response = requests.request(**kwargs)
        except requests.ConnectionError as exc:
            raise SincproExternalServiceError(f"Connection error to {url}") from exc
        except requests.Timeout as exc:
            raise SincproExternalServiceError(f"Request timed out for {url}") from exc
        except requests.TooManyRedirects as exc:
            raise SincproExternalServiceError(f"Too many redirects for {url}") from exc

        try:
            response.raise_for_status()
        except HTTPError as error:
            http_code = error.response.status_code
            reason = error.response.reason
            content = getattr(error.response, "content", None)
            text = getattr(error.response, "text", None)

            raise SincproHTTPError(
                str(error),
                http_code=http_code,
                reason=reason,
                content=content,
                text=text,
            ) from error
        return response

    @staticmethod
    def safe_parse_json(response: Response) -> dict:
        """Parse JSON response, wrapping decode errors in SincproExternalServiceError."""
        try:
            return response.json()
        except (ValueError, TypeError) as exc:
            raise SincproExternalServiceError(
                f"Failed to parse JSON response (HTTP {response.status_code})"
            ) from exc