Домой Edit me on GitHub

2017-10-18

REST API

REST API подразумевает под собой простые правила:

  • Каждый URL является ресурсом
  • При обращении к ресурсу методом GET возвращается описание этого ресурса
  • Метод POST добавляет новый ресурс
  • Метод PUT изменяет ресурс
  • Метод DELETE удаляет ресурс

Эти правила предоставляют простой CRUD интерфейс для других приложений, взаимодействие с которым происходит через протокол HTTP.

Соответствие CRUD операций и HTTP методов:

  • CREATE - POST
  • READ - GET
  • UPDATE - PUT
  • DELETE - DELETE

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

Pattern matching

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from wsgiref.simple_server import make_server

from pyramid.view import view_config, view_defaults
from pyramid.config import Configurator


@view_defaults(
    route_name='rest_people',
    renderer='json'
)
class RESTViewPeople(object):
    def __init__(self, request):
        self.request = request

    @view_config(request_method='GET')
    def get(self):
        return {
            'id': self.request.matchdict['id'],
            'method': self.request.method,
            'get': dict(self.request.GET)
        }

    @view_config(request_method='POST')
    def post(self):
        return {
            'id': self.request.matchdict['id'],
            'method': self.request.method,
            'post': dict(self.request.POST)
        }

    @view_config(request_method='DELETE')
    def delete(self):
        return {'status': 'success'}


if __name__ == '__main__':
    config = Configurator()
    config.add_route('rest_people', '/api/v1/people/{id:\d+}')
    config.add_view(RESTViewPeople, route_name='rest_people')
    config.scan('.')

    # make wsgi app
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Пример выше добавляет View с тремя методами, каждый из которых вызывается при соответствующем GET, POST, DELETE запросе. Ресурсом здесь является конкретный человек, получить которого можно по URL http://localhost:8080/api/v1/people/123

Результатом запроса будет:

{"get": {}, "id": "123", "method": "GET"}

Для отправки POST запроса воспользуемся консольной утилитой curl:

$ curl -X POST -d 'param1=value1&param2=value2' http://localhost:8080/api/v1/people/1

Результат запроса:

{"id": "1", "post": {"param1": "value1", "param2": "value2"}, "method": "POST"}

DELETE запрос выполняется по аналогии:

$ curl -X DELETE http://localhost:8080/api/v1/people/1

Результат запроса:

{"status": "success"}

Traversal

См.также

Метод URL диспетчеризации Traversal

В предыдущем примере показан только один ресурс - конкретный человек и в принципе все выглядит неплохо, пока не появится другой смежный ресурс, например список всех людей по адресу http://localhost:8080/api/v1/people

В этом случае, придется добавлять новый путь (rout), привязывать его к представлению (View) и самое неприятное менять само представление, или еще хуже писать новое. Таким образом с увеличением ресурсов, сложность REST API растет не пропорционально и в какой то момент код становится не читаемым из-за больших размеров и постоянно меняющейся логики во View.

Выход из данной ситуации - отделить ресурсы от представлений, тем самым вынести часть логики и сделать представления более универсальными.

Ресурсы

Ресурсы могут выглядеть так:

Список всех людей
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class PeopleResource(object):

    def __getitem__(self, people_id):
        if str(people_id).isdigit():
            return PersonResource(people_id)

    def __json__(self, request):
        return {
            'params': request.matchdict,
            'method': request.method,
        }

PeopleResource представляет список всех людей и будет доступен по адресу http://localhost:8080/api/v1/people. PeopleResource имеет метод __getitem__, что делает его похожим на словарь. При обращении к объекту ресурса как к словарю, он вызовет эту функцию и передаст ключ в параметр people_id, например:

foo = PeopleResource()
bar = foo[123]  # Вернет объект PersonResource(123)

Метод __json__ определяет каким образом преобразовывать ресурс в json.

PersonResource представляет конкретного человека и будет доступен по адресу http://localhost:8080/api/v1/people/{id}. Здесь отличительной особенностью является то, что метод __json__ наследует часть словаря из класса PeopleResource, при помощи конструкции super:

Конкретный человек
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class PersonResource(PeopleResource):

    def __init__(self, people_id):
        self.id = people_id

    def __json__(self, request):
        return {
            'id': self.id,
            **super().__json__(request)
        }

View

Перепишем View таким образом, чтобы она возвращала только ресурс, а так-как ресурс уже содержит в себе информацию как отдавать json, то это представление будет универсальным как для PeopleResource, так и для PersonResource и возможно подойдет другим ресурсам которые мы будем писать в будущем.

Представление (View) для traversal ресурсов
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@view_defaults(
    route_name='rest_api',
    renderer='json',
    context=PeopleResource
)
class RESTViewPeople(object):
    def __init__(self, context, request):
        self.context = context
        self.request = request

    @view_config(request_method='GET')
    def get(self):
        return self.context

    @view_config(request_method='POST')
    def post(self):
        return self.context

    @view_config(request_method='DELETE')
    def delete(self):
        return {'status': 'success'}

Рендерер json по умолчанию ищет метод __json__ и если он есть то возвращает его результат вызова.

Route

Путь, в нашем случае, будет один, так-как вся структура вынесена в ресурсы (метод __getitem__).

config.add_route('rest_api', '/api/v1/*traverse', factory=rest_factory)

Полный пример

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from wsgiref.simple_server import make_server

from pyramid.view import view_config, view_defaults
from pyramid.config import Configurator


class PeopleResource(object):

    def __getitem__(self, people_id):
        if str(people_id).isdigit():
            return PersonResource(people_id)

    def __json__(self, request):
        return {
            'params': request.matchdict,
            'method': request.method,
        }


class PersonResource(PeopleResource):

    def __init__(self, people_id):
        self.id = people_id

    def __json__(self, request):
        return {
            'id': self.id,
            **super().__json__(request)
        }


class AnimalsResource(object):
    pass


@view_defaults(
    route_name='rest_api',
    renderer='json',
    context=PeopleResource
)
class RESTViewPeople(object):
    def __init__(self, context, request):
        self.context = context
        self.request = request

    @view_config(request_method='GET')
    def get(self):
        return self.context

    @view_config(request_method='POST')
    def post(self):
        return self.context

    @view_config(request_method='DELETE')
    def delete(self):
        return {'status': 'success'}


def rest_factory(request):
    return {
        'people': PeopleResource(),
        'animals': AnimalsResource(),
    }


if __name__ == '__main__':
    config = Configurator()
    config.add_route('rest_api', '/api/v1/*traverse', factory=rest_factory)
    config.add_view(RESTViewPeople, route_name='rest_api')
    config.scan('.')

    # make wsgi app
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()
Previous: Диспетчеризация URL Next: Предстваления (Views)