Класс коннектор для Диадок API на Python

от автора

Решил поделиться своим опытом как я собирался сделать сервис управлением ЭДО провайдерами по правилам SOLID.

Для начала я решил составить архитектуру сервиса, решил что класс управления api должен включать в себя http клиент как зависимость, так как не все могут захотеть использовать requests для выполнения запросов, еще это даст возможность переехать на асинхронную версию. Изучив документацию системы Диадок, я узнал что запросы можно выполнять как в JSON формате так и используя RPC модели. Поэтому я назвал класс DiadocJSONClient и он использует библиотеку requests для http запросов.

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)

Немного расскажу об основных методах класса

  • __init__принимает креды для авторизации

  • get — выполняет GET запрос к Диадоку

  • post — выполняет POST запрос к Диадоку. Изначально в методе (как и в get) проверяется есть ли открытая авторизованная сессия, если нет то создается сессия в методе класса. Это сделано для того чтобы можно было выполнять несколько запросов получив токен один раз на всю сессию. если запрос создается вне сессии то сессия создастся на этот запрос и закроется после выполнения метода.

Диадок выдает не всю информацию в теле ответа, иногда некоторые параметры передаются в headers ответа. Ввиду этого мне пришлось сделать класс для ответов клиента, чтобы независимо от используемой библиотеки для запросов, ответ должен был иметь одинаковые методы.
Я создал свойство response_obj = RequestsResponse

Интерфейс модели ответа

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 

Сама модель ответа

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()

Все ответы метода get и post привожу к свой общей модели,
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)

Тут я описал структуру моего клиента для запросов к API Диадок. В следующей статье я опишу как этот клиент встраивается в класс провайдера, который уже непосредственно выполняет запросы и обрабатывает ответы.


ссылка на оригинал статьи https://habr.com/ru/articles/799247/