Connector class for Diadoc API in Python

I decided to share my experience of how I was going to make a service for EDI management by providers according to SOLID rules.

To begin with, I decided to draw up the architecture of the service, I decided that the api control class should include the http client as a dependency, since not everyone may want to use requests to execute requests, and this will also make it possible to move to the asynchronous version. Having studied the documentation of the Diadoc system, I learned that requests can be executed both in JSON format and using the RPC model. So I named the class DiadocJSONClient and it uses the requests library for http requests.

class DiadocJSONClient:
    """Клиент АПИ запросов."""
    
    session = None
    response_obj = RequestsResponse

    def __init__(
        self,
        url: str,
        login: str = None,
        password: str = None,
        api_client_id: str = None,
    ):
        self.url = url
        self._login = login
        self._password = password
        self._api_client_id = api_client_id

    def __enter__(self):
        logger.info("Create client connection")
        created, self.session = self._session_get_or_create()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        logger.info("Close client connection!")
        self._close_session()

    def _create_session(self):
        """Создать сессию."""
        token = self._get_token()
        headers = self._get_headers(token)
        session = requests.Session()
        session.headers.update(headers)
        logger.success("Session created!")
        return session

    def _close_session(self):
        """Закрыть сессию."""
        self._check_session_is_exists()
        self.session.close()

    def _session_get_or_create(self):
        if not self.session:
            logger.info("SESSION NOT FIND!")
            return True, self._create_session()
        return False, self.session

    def _get_token(self) -> str:
        """Получить токен."""
        url = f"{self.url}/V3/Authenticate"

        auth_str = f"DiadocAuth ddauth_api_client_id={self._api_client_id}"
        headers = {"Authorization": auth_str}
        params = {"type": "password"}
        body = {"login": self._login, "password": self._password}

        try:
            response = requests.post(
                url, headers=headers, params=params, json=body
            )
            response.raise_for_status()
        except Exception as err:
            logger.error("{}: {}", err.__class__.__name__, err)
            raise TokenReceiptError(
                "Ошибка получения токена: {}".format(err.__class__.__name__)
            )

        token = response.text

        return token

    def _get_headers(self, token: str) -> dict:
        """Получить headers."""
        auth_str = (
            "DiadocAuth "
            f"ddauth_api_client_id={self._api_client_id}, "
            f"ddauth_token={token}"
        )
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": auth_str,
        }
        return headers

    def post(self, method, body=None, params=None) -> HTTPResponse:
        """POST запрос."""
        created, session = self._session_get_or_create()

        body = body or {}
        params = params or {}
        request_kwargs = {"params": params, "json": body}
        url = f"{self.url}/{method}"
        logger.debug("POST /{}, body={}, params={}", method, body, params)
        try:
            response = session.post(url, **request_kwargs)
            logger.debug(response.request.headers)
            response.raise_for_status()
        except Exception as err:
            logger.error("{}: {}", err.__class__.__name__, err)
            try:
                error_text = response.text
                logger.debug(error_text)
            except Exception:
                error_text = ""
            # logger.debug(response.request.body)
            raise RequestError(
                f"Ошибка выполнения запроса: "
                f"POST /{method}: {err}, "
                f"text: {error_text}"
            )

        if created:
            session.close()

        logger.debug("{}: {}", response.status_code, response.text)
        return self.response_obj(response)

    def post_binary(
        self,
        method,
        params=None,
        files_content: bytes = None,
    ):
        """POST запрос."""
        created, session = self._session_get_or_create()
        params = params or {}

        url = f"{self.url}/{method}"
        logger.debug("POST BINARY /{}, params={}", method, params)
        try:
            response = session.post(url, params=params, data=files_content)
            logger.debug(response.request.headers)
            response.raise_for_status()
        except Exception as err:
            logger.error("{}: {}", err.__class__.__name__, err)
            try:
                error_text = response.text
                logger.debug(error_text)
            except Exception:
                error_text = ""
            # logger.debug(response.request.body)
            raise RequestError(
                f"Ошибка выполнения запроса: "
                f"POST /{method}: {err}, "
                f"text: {error_text}"
            )

        if created:
            session.close()

        logger.debug("{}: {}", response.status_code, response.text)
        return self.response_obj(response)

    def get(self, method, params=None, headers=None) -> HTTPResponse:
        """GET запрос."""
        created, session = self._session_get_or_create()
        params = params or {}
        headers = headers or {}
        session.headers.update(headers)
        url = f"{self.url}/{method}"
        logger.debug("GET /{}, params={}", url, params)
        try:
            response = session.get(url, params=params)
            logger.debug(response.request.headers)
            response.raise_for_status()
        except Exception as err:
            logger.error("{}: {}", err.__class__.__name__, err)
            raise RequestError(
                f"Ошибка выполнения запроса: GET /{method}: {err}"
            )
        if created:
            session.close()

        logger.debug("response: {}", response.text[:200])
        return self.response_obj(response)

I'll tell you a little about the main methods of the class

  • __init__accepts credentials for authorization

  • get – performs a GET request to Diadoc

  • post – performs a POST request to Diadoc. Initially, the method (as in get) checks whether there is an open authorized session; if not, then a session is created in the class method. This is done so that you can perform several requests by receiving the token once for the entire session. if a request is created outside of a session, then a session will be created for this request and will be closed after the method is executed.

Diadoc does not display all the information in the response body; sometimes some parameters are passed to the response headers. In view of this, I had to make a class for client responses so that, regardless of the library used for requests, the response should have the same methods.
I created a property response_obj = RequestsResponse

Response Model Interface

from abc import ABC, abstractmethod
from typing import Any


class HTTPResponse(ABC):
    @abstractmethod
    def __init__(self, response: Any):
        self._response = response

    @property
    @abstractmethod
    def status_code(self) -> int:
        pass

    @property
    @abstractmethod
    def headers(self) -> dict[str, str]:
        pass

    @property
    @abstractmethod
    def content(self) -> bytes:
        pass

    @property
    @abstractmethod
    def text(self) -> bytes:
        pass

    @abstractmethod
    def json(self) -> Any:
        pass

    def raise_for_status(self) -> None:
        pass

The response model itself

class RequestsResponse(HTTPResponse):
    def __init__(self, response: Response):
        self._response = response

    @property
    def status_code(self) -> int:
        return self._response.status_code

    @property
    def headers(self) -> dict[str, str]:
        return dict(self._response.headers)

    @property
    def content(self) -> bytes:
        return self._response.content

    @property
    def text(self) -> str:
        return self._response.text

    def json(self) -> dict:
        return self._response.json()

    def raise_for_status(self) -> None:
        self._response.raise_for_status()

I bring all the responses of the get and post method to my general model,
return self.response_obj(response)

    def get(self, method, params=None, headers=None) -> HTTPResponse:
        """GET запрос."""
        created, session = self._session_get_or_create()
        params = params or {}
        headers = headers or {}
        session.headers.update(headers)
        url = f"{self.url}/{method}"
        logger.debug("GET /{}, params={}", url, params)
        try:
            response = session.get(url, params=params)
            logger.debug(response.request.headers)
            response.raise_for_status()
        except Exception as err:
            logger.error("{}: {}", err.__class__.__name__, err)
            raise RequestError(
                f"Ошибка выполнения запроса: GET /{method}: {err}"
            )
        if created:
            session.close()

        logger.debug("response: {}", response.text[:200])
        return self.response_obj(response)

Here I described the structure of my client for requests to the Diadoc API. In the next article I will describe how this client is built into the provider class, which directly executes requests and processes responses.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *