Flask-RESTful API: множественные и сложные конечные точки

Представьте, что в моем API Flask-RESTful у меня есть два объекта: пользователи и города. Это отношения "один ко многим". Теперь, когда я создаю свой API и добавляю к нему ресурсы, все, что я могу сделать, это сопоставить с ними очень простые и общие URL-адреса. Вот код (без ненужных вещей):

class UserAPI(Resource):  # The API class that handles a single user
  def __init__(self):
    # Initialize

  def get(self, id):
    # GET requests

  def put(self, id):
    # PUT requests

  def delete(self, id):
    # DELETE requests

class UserListAPI(Resource):  # The API class that handles the whole group of Users
  def __init__(self):

  def get(self):

  def post(self):

api.add_resource(UserAPI, '/api/user/<int:id>', endpoint='user')
api.add_resource(UserListAPI, '/api/users/', endpoint='users')

class CityAPI(Resource):
  def __init__(self):

  def get(self, id):

  def put(self, id):

  def delete(self, id):

class CityListAPI(Resource):
  def __init__(self):

  def get(self):

  def post(self):

api.add_resource(CityListAPI, '/api/cities/', endpoint='cities')
api.add_resource(CityAPI, '/api/city/<int:id>', endpoint='city')

Как видите, я могу делать все, что хочу, чтобы реализовать самые простые функции. Я могу GET, POST, PUT и DELETE оба объекта. Однако у меня двоякая цель:

(1) Чтобы иметь возможность запрашивать с другими параметрами, такими как название города, а не просто идентификатор города. Это выглядело бы примерно так:
api.add_resource(CityAPI, '/api/city/<string:name>', endpoint='city')
за исключением того, что это не выдало бы мне такую ​​ошибку:

AssertionError: отображение функции просмотра перезаписывает существующую функцию конечной точки

(2) Чтобы иметь возможность объединить два ресурса в запросе. Скажем, я хотел связать всех пользователей с каким-то городом. В URL-адресах REST это должно выглядеть примерно так:
/api/cities/<int:id>/users

Как мне это сделать с Flask? С какой конечной точкой мне его сопоставить?

По сути, я ищу способы превратить свой API из базового в полезный.


person Alex Chumbley    schedule 21.12.2013    source источник


Ответы (2)


Вы делаете две ошибки.

Во-первых, Flask-RESTful наводит на мысль, что ресурс реализован с одним URL-адресом. На самом деле у вас может быть много разных URL-адресов, которые возвращают ресурсы одного типа. В Flask-RESTful вам нужно будет создать другой подкласс Resource для каждого URL-адреса, но концептуально эти URL-адреса принадлежат одному и тому же ресурсу. Обратите внимание, что на самом деле вы уже создали по два экземпляра для каждого ресурса для обработки списка и отдельных запросов.

Вторая ошибка, которую вы делаете, заключается в том, что вы ожидаете, что клиент знает все URL-адреса в вашем API. Это не лучший способ создания API-интерфейсов, в идеале клиент знает только несколько URL-адресов верхнего уровня, а затем обнаруживает остальные из данных в ответах от URL-адресов верхнего уровня.

В своем API вы можете захотеть предоставить /api/users и /api/cities как API верхнего уровня. В ответы будут включены URL-адреса отдельных городов и пользователей. Например, если я вызываю http://example.com/api/users, чтобы получить список пользователей, я могу получить такой ответ:

{
    "users": [ 
        {
            "url": "http://example.com/api/user/1",
            "name": "John Smith",
            "city": "http://example.com/api/city/35"
        },
        {
            "url": "http://example.com/api/user/2",
            "name": "Susan Jones",
            "city": "http://example.com/api/city/2"
        }
    ]
}

Обратите внимание, что представление пользователя в формате JSON включает URL-адрес этого пользователя, а также URL-адрес города. Клиенту не нужно знать, как их строить, потому что они ему даны.

Получение городов по названию

URL-адрес города - /api/city/<id>, а URL-адрес для получения полного списка городов - /api/cities, как вы его определили.

Если вам также нужно искать города по их названию, вы можете расширить для этого конечную точку «cities». Например, у вас могут быть URL-адреса в форме /api/cities/<name>, возвращающие список городов, соответствующих поисковому запросу, заданному как <name>.

С Flask-RESTful вам нужно будет определить для этого новый подкласс Resource, например:

    class CitiesByNameAPI(Resource):
        def __init__(self):
            # ...    
        def get(self, name):
            # ...

    api.add_resource(CitiesByNameAPI, '/api/cities/<name>', endpoint = 'cities_by_name')

Получение всех пользователей, принадлежащих к городу

Когда клиент запрашивает город, он должен получить ответ, включающий URL-адрес для получения пользователей в этом городе. Например, предположим, что из ответа /api/users выше я хочу узнать город первого пользователя. Итак, теперь я отправляю запрос на http://example/api/city/35 и получаю следующий ответ JSON:

{
    "url": "http://example.com/api/city/35",
    "name": "San Francisco",
    "users": "http://example/com/api/city/35/users"
}

Теперь у меня есть город, и это дало мне URL-адрес, по которому я могу получить всех пользователей в этом городе.

Обратите внимание, что не имеет значения, что ваши URL-адреса уродливы или сложны для построения, потому что клиенту никогда не нужно создавать большинство из них с нуля, он просто получает их с сервера. Это также позволяет вам изменить формат URL-адресов в будущем.

Чтобы реализовать URL-адрес, который получает пользователей по городу, вы добавляете еще один подкласс Resource:

    class UsersByCityAPI(Resource):
        def __init__(self):
            # ...    
        def get(self, id):
            # ...

    api.add_resource(UsersByCityAPI, '/api/cities/<int:id>/users', endpoint = 'users_by_city')

Надеюсь, это поможет!

person Miguel    schedule 21.12.2013
comment
Замечательно, спасибо за исправления. Также спасибо за предложение скрыть большую часть моего API - person Alex Chumbley; 22.12.2013
comment
Хотя Мигель в значительной степени прав, я думаю, важно отметить, что [ожидание, что клиент знает все URL-адреса в вашем API] - не лучший способ создания API - это просто одно мнение по этому поводу. Предварительное определение URL-адресов не вызовет синтаксической ошибки и не приведет к сбою приложения; это дизайнерское решение. Вопрос о том, лучше ли использовать определенный список URL-адресов или просто API верхнего уровня с URL-адресами на подуровне API (или какое-либо другое решение), все еще обсуждается, и правильный ответ все еще может быть проектом -конкретный. - person Mark Hildreth; 22.12.2013
comment
@MarkHildreth: Согласен. - person Miguel; 22.12.2013
comment
Интересный взгляд на эту тему. Вы думаете, что тот факт, что только я планирую использовать этот API, имеет значение? Итак, если этот API предназначен для использования в мобильном приложении, которое я не планирую раскрывать другим людям, это означает, что я могу безопасно раскрыть все свои URL-адреса? - person Alex Chumbley; 22.12.2013
comment
Открытие API-интерфейсов или нет - это не проблема безопасности, это вопрос инкапсуляции. Чем больше вы обнародуете, тем меньше у вас будет возможностей измениться в будущем. Кроме того, для клиента больше работы создавать URL-адреса из идентификаторов, чем создавать URL-адреса на сервере с помощью url_for() и передавать их клиенту, готовым к использованию. - person Miguel; 23.12.2013

вы можете использовать id / name без дублирования ресурса:

api.add_resource(CitiesByNameAPI, '/api/cities/<name_or_id>', endpoint = 'cities_by_name')

class CitiesByNameAPI(Resource):
    def get(self, name_or_id):
        if name_or_id.isdigit():
            city = CityModel.find_by_id(name_or_id)
        else:
            city = CityModel.find_by_name(name_or_id)

        if city:
            return city.to_json(), 200
        return {'error': 'not found'}, 404

не уверен, есть ли от этого какие-либо негативные последствия.

person lciamp    schedule 13.09.2018