Ниже в демонстрационных и тестовых целях приведет код тестового клиента API CASHOFF, написанные на python версии 3. Он содержит примеры подписи, формирования заголовков и url для вызовов. Далее, примеры использования и непосредственно исходный код:
Пример использования
c = CashoffAPIClient('https://developer.cashoff.ru', 'test', 'test')
c.request('/users')
c.request('/users/add', body={'ext_id': '123456'})
c.request('/accounts/5612', method='DELETE')
c.request('/accounts', {'profile_id': 5235})
c.request('/profiles/add', body={'provider_key': 'sber'}, user_id=2512)
Исходный код
import hashlib
import hmac
import json
import time
from base64 import b64encode
from http.client import HTTPConnection, HTTPSConnection
from urllib.parse import urlencode, urlsplit, quote
class CashoffAPIClient:
"""Базовый клиент для работы с CASHOFF API через python3."""
def __init__(self, host, login, secret, use_sign=True):
"""
:param host: Адрес, с указанием протокола
:param login: логин (ServiceID)
:param secret: пароль (PrivateKey)
:param use_sign: использовать для аутентификации подпись; В случае False будет использовано http basic auth
"""
url_parts = urlsplit(host)
connection_cls = HTTPConnection if url_parts.scheme == 'http' else HTTPSConnection
self.connection = connection_cls(url_parts.hostname, url_parts.port)
self.login = login
self.secret = secret
self.use_sign = use_sign
def request(self, url, params=None, body=None, *, user_id=None, session=None, method=None):
"""
Выполнить GET/POST запрос
:param url: относительный путь к запросу, как в документации
:param params: get-параметры запроса в виде dict()
:param body: тело, если POST; автоматически конвертируется в json
:param user_id: идентификатор юзера; подставится в заголовок co-user-id
:param session: идентификатор сессии; подставится в заголовок co-session, будет использоваться вместо подписи
:param method: http-метод; по умолчанию будет подставлен get/post, в зависимости от body
:return: пара (http код, распарсеное тело ответа/текст в случае неудачи)
"""
url = self._format_request_url(url, params)
if method is None:
method = 'GET' if body is None else 'POST'
if body is not None:
body = json.dumps(body, ensure_ascii=False)
headers = self._make_headers(url, body, user_id, session)
if body is not None:
body = body.encode('utf-8')
try:
self.connection.request(method, url, body, headers)
except ConnectionResetError:
self.connection.close()
self.connection.request(method, url, body, headers)
response = self.connection.getresponse()
result = response.read().decode('utf-8')
result = result and json.loads(result)
return response.status, result
def _make_headers(self, url, body, user_id=None, session=None):
"""Генерирует заголовки для запроса"""
headers = {
'Content-type': 'application/json',
}
if session:
headers['co-session'] = session
else:
if self.use_sign:
headers['co-auth'] = self._make_sign(url, body, user_id)
else:
headers['co-auth'] = 'http_basic'
headers['Authorization'] = self._make_http_basic()
if user_id is not None:
headers['co-user-id'] = str(user_id)
return headers
def _make_sign(self, url, body, user_id):
timestamp = int(time.time())
msg = (
self.login +
str(timestamp) +
(str(user_id) if user_id is not None else '') +
self._get_url_for_sign(url) +
(body or '')
)
signature = hmac.new(
self.secret.encode('utf-8'), msg.encode('utf-8'), hashlib.sha1
)
return f'sha1|{self.login}:{timestamp}:{signature.hexdigest()}'
def _make_http_basic(self):
auth_data = quote(self.login) + ':' + quote(self.secret)
return 'Basic ' + b64encode(auth_data.encode()).decode()
def _get_url_for_sign(self, url):
parts = urlsplit(url)
url_for_sign = parts.path
if parts.query:
url_for_sign += '?' + parts.query
return url_for_sign
def _format_request_url(self, url, params):
if url.startswith('/'):
url = url[1:]
if params is not None:
url += '?' + urlencode(params)
return '/api/v2/' + url