Skip to content

Latest commit

 

History

History
148 lines (86 loc) · 8.27 KB

File metadata and controls

148 lines (86 loc) · 8.27 KB

Как писать клиентские библиотеки для API

Данный документ описывает практики, которые мы применяем в forkode.ru для разработки клиентских библиотек для API.

Общее для любого клиента

Исключения

Хорошее API должно скрывать факт использования requests и других бибоитек предоставляя более высокоуровневые исключения для пользователя.

Все исключения должы быть помещены в отдельный файл, например, exceptions.py. В нем должно быть объявлено одно общее исключение, например, SomeClientError и все остальные исключения должны быть унаследованы от него. Это необходимо для того, чтобы пользователь мог в одном except: блоке поймать все возможные исключения библиотеки.

При обработке исключений requests и других библиотек не стоит забывать про Exception Chaining.

Логгирование

Логгирование должно быть реализовано стандартным для Python образом. В начале каждого файла создается логгер, которые используется в коде.

logger = logging.getLogger(__name__)

...

logger.debug(..)

REST API на основе requests

Клиетская библиотека для 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 ...