web-dev-qa-db-fra.com

Flask API RESTful points de terminaison multiples et complexes

Dans mon API Flask-RESTful, imaginez que j'ai deux objets, les utilisateurs et les villes. Il s'agit d'une relation 1 à plusieurs. Maintenant, quand je crée mon API et que j'y ajoute des ressources, tout ce que je peux faire est de mapper des URL très simples et générales vers elles. Voici le code (avec des trucs inutiles non inclus):

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')

Comme vous pouvez le voir, je peux faire tout ce que je veux pour implémenter des fonctionnalités très basiques. Je peux obtenir, publier, mettre et supprimer les deux objets. Cependant, mon objectif est double:

(1) Pour pouvoir demander avec d'autres paramètres comme le nom de la ville au lieu de simplement l'identifiant de la ville. Cela ressemblerait à quelque chose comme:
api.add_resource(CityAPI, '/api/city/<string:name>', endpoint='city')
sauf que cela ne me ferait pas tomber cette erreur:

AssertionError: le mappage de fonction d'affichage remplace une fonction de point de terminaison existante

(2) Pour pouvoir combiner les deux ressources dans une demande. Supposons que je souhaite obtenir tous les utilisateurs associés à une ville. Dans les URL REST, cela devrait ressembler à:
/api/cities/<int:id>/users

Comment faire ça avec Flask? À quel point de terminaison dois-je le mapper?

Fondamentalement, je cherche des moyens de faire passer mon API de basique à utilisable. Merci pour toutes idées/conseils

29
Alex Chumbley

Vous faites deux erreurs.

Tout d'abord, Flask-RESTful vous fait penser qu'une ressource est implémentée avec une seule URL. En réalité, vous pouvez avoir de nombreuses URL différentes qui renvoient des ressources du même type. Dans Flask-RESTful, vous devrez créer une sous-classe Resource différente pour chaque URL, mais conceptuellement, ces URL appartiennent à la même ressource. Notez que vous avez en fait déjà créé deux instances par ressource pour gérer la liste et les demandes individuelles.

La deuxième erreur que vous faites est que vous vous attendez à ce que le client connaisse toutes les URL de votre API. Ce n'est pas un bon moyen de créer des API, idéalement, le client ne connaît que quelques URL de niveau supérieur, puis découvre le reste à partir des données dans les réponses des URL de niveau supérieur.

Dans votre API, vous souhaiterez peut-être exposer le /api/users et /api/cities comme API de niveau supérieur. Les URL des différentes villes et utilisateurs seront incluses dans les réponses. Par exemple, si j'invoque http://example.com/api/users pour obtenir la liste des utilisateurs, je peux obtenir cette réponse:

{
    "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"
        }
    ]
}

Notez que la représentation JSON d'un utilisateur inclut l'URL de cet utilisateur, ainsi que l'URL de la ville. Le client n'a pas besoin de savoir comment les construire, car ils lui sont donnés.

Obtenir les villes par leur nom

L'URL d'une ville est /api/city/<id>, et l'URL pour obtenir la liste complète des villes est /api/cities, tel que vous l'avez défini.

Si vous devez également rechercher des villes par leur nom, vous pouvez étendre le point de terminaison "villes" pour ce faire. Par exemple, vous pouvez avoir des URL sous la forme /api/cities/<name> renvoie la liste des villes qui correspondent au terme de recherche donné par <name>.

Avec Flask-RESTful, vous devrez définir une nouvelle sous-classe Resource pour cela, par exemple:

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

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

Obtenir tous les utilisateurs appartenant à une ville

Lorsque le client demande une ville, il doit obtenir une réponse qui inclut une URL pour obtenir les utilisateurs de cette ville. Par exemple, disons qu'à partir du /api/users réponse ci-dessus Je souhaite connaître la ville du premier utilisateur. Alors maintenant, j'envoie une demande à http://example/api/city/35, et je récupère la réponse JSON suivante:

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

Maintenant, j'ai la ville, et cela m'a donné une URL que je peux utiliser pour obtenir tous les utilisateurs de cette ville.

Notez que cela n'a pas d'importance que vos URL soient laides ou difficiles à construire, car le client n'a jamais besoin d'en créer la plupart à partir de zéro, il les obtient simplement du serveur. Cela vous permet également de modifier le format des URL à l'avenir.

Pour implémenter l'URL qui obtient les utilisateurs par ville, vous ajoutez encore une autre sous-classe Resource:

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

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

J'espère que ça aide!

64
Miguel

vous pouvez faire la chose id/name sans dupliquer la ressource:

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

je ne sais pas s'il y a des effets négatifs de cela.

1
lciamp