Данный документ описывает практики, которые мы применяем в forkode.ru для разработки клиентских библиотек для API.
Хорошее API должно скрывать факт использования requests и других бибоитек предоставляя более высокоуровневые исключения для пользователя.
Все исключения должы быть помещены в отдельный файл, например, exceptions.py. В нем должно быть объявлено одно общее исключение, например, SomeClientError и все остальные исключения должны быть унаследованы от него. Это необходимо для того, чтобы пользователь мог в одном except: блоке поймать все возможные исключения библиотеки.
При обработке исключений requests и других библиотек не стоит забывать про Exception Chaining.
Логгирование должно быть реализовано стандартным для Python образом. В начале каждого файла создается логгер, которые используется в коде.
logger = logging.getLogger(__name__)
...
logger.debug(..)Клиетская библиотека для REST API, которая использует requests в своей основе это самый простой и, одновременно, удобный с точки зрения использования вид библиотек.
Основа библиотеки это класс, который принимает все необходимые настройки в конструкторе и содержит все методы API как методы класса.
Пример:
class SomeClient:
def __init__(self, base_url='https://api-url.com'):
self.base_url = base_url
def method1(self, arg1):
....
def method2(self, arg1, arg2):
....Если библиотека используется в разных окружениях, то может существовать потребность в переопределении настроек для разных окружений. Это допустимо делать используя переменные окружения.
Пример:
import os
class SomeClient:
DEFAULT_BASE_URL = 'https://api-url.com'
def __init__(self, base_url=None):
if base_url is None:
if 'SOME_API_BASE_URL' in os.environ:
base_url = os.environ['SOME_API_BASE_URL']
else:
base_url = self.DEFAULT_BASE_URLОднако, такой подход делает неочевидным для пользователя какое значение примет base_url, поэтому он не является повсеместно рекомендуемым.
Каждый метод в клиенте должен быть очевидным для пользователя. Его назначение, входные агрументы и реузльтат должны быть понятны из сигнатуры и документации метода без чтения исходного кода метода или обращения к документации по API.
Если библиотека будет использоваться только в коде на Python версии 3.5+, то рекомендуется использовать type hints т. к. это делает код значительно более читаемым.
Обычно методы API возвращают JSON, который очевидным образом преобразуется в базовые структуры данных Pyton. Для небольших API допустимо возвращать эти данные в первоначальном виде, однако, должно существовать описание их структуры. Более серьезным и рекомендуемым подходом является использование dataclasses для Python версии 3.6+ и namedtuple для Pytohn версии 2.7.
Если API предоставляет множество методов и код разрастается в одном файле, то рекомендуется разделить методы на множество классов и объединить их в одном через наследоование. Пример такого подхода в клиенте Telethon
Данный подход применяется, котогда необхдоимо реализовать клиента, котрый будет работать на python2.7, python3+ и в двух вариантах - синхронном с requests и асинхронном c asyncio. Для достижения такой универсальности необходимо отделить содержание запросов/ответов от клиента, который их исполняет. Таким образом, код запросов/ответов будет общим для всех библиотек, а код исполняющего их клиента будет зависеть от той или иной библиотеки.
Для каждого запроса пишется отдельный класс, который принимает в конструкторе все необходимые данные для проведения запроса. Если несколько запросов принимат одинаковые данные, то их следует выносить в отдельные классы. Таким же образом создается по классу на каждый ответ если он не соотвествует простому типу данных.
Пример:
Card = namedtuple('Card', ['pan', 'year', 'month', 'cvv'])
class InitPayment:
method = 'POST'
path = '/billing/init-payment/'
def __init__(self, card, amount):
self.card = card
self.amount = amount
def as_dict(self):
return {
'PAN': self.card.pan,
'YEAR': self.card.year,
'MONTH': self.card.month,
'CVV': self.card.cvv,
'AMOUNT': self.amount
}
class InitPaymentResult:
def __init__(self, transaction_id, secret):
self.transaction_id = transaction_id
self.secret = secret
Основа библиотеки это класс, который принимает все необходимые настройки в конструкторе и содержит метод call, который принимает на вход любой из классов содержащих запрос.
Пример:
class SomeClient:
def __init__(self, base_url='https://api-url.com'):
self.base_url = base_url
def call(self, request):
result = requests.request(request.method, request.path, data=request.as_dict())
...
return ...