Thématiques principales

mardi 6 août 2019

Rest avec Python


Après de nouveau un bon mois sans rien avoir poster dans le blog, voici la suite de l'article précédent sur ReST [rest-th] mais en rentrant un peu plus dans le concret.

Ainsi je vous propose de voir ce que python nous propose pour aborder cette problématique de réalisation ou d'interrogation d’une interface ReST.

Pour faire simple, nous allons considérer une population d’individu sur laquelle nous allons collecter des informations et que nous exposerons via une première interface ReST. Grâce à ce référentiel, nous pousserons un peu plus loin le travail en proposant une seconde interface ReST dont la logique ici sera d’exploiter la première interface afin de fournir divers indicateurs.



Le schema precedent présente les deux applications et leur environnement. L’idée est simple l’une expose des données génériques, l’autre les exploite pour exposer des données de plus haut niveau.

Rest-people

Cette partie de l’application est celle présentant les données à plat. Elle s’alimente sur une base de données (que l’on simplifiera par un liste par soucis de simplicité) et les exposera via deux interfaces ReST, un GET permettant de récupérer la liste complément des individu de la base et un GET permettant d’en sélectionner un seul en fonction de son Id. On complètera cette application par une méthode POST pour ajouter un élément à la population.

En python pour faire cela, on va commencer par créer des humains, des hommes et des femmes en faisant des classes python très classiques.


class Human:

    def __init__(self, id, name, age):
        self.id = id
        self.name = name
        self.age = age

    def __getitem__(self, name):
        return self.__getattribute__(name)

    def __setitem__(self, name, valeur):
        return self.__setattr__(name, valeur)

    def __delitem__(self):
        return ","


class Men(Human):

    def __init__(self, id, name, age):
        Human.__init__(self, id, name, age)


class Women(Human):

    def __init__(self, id, name, age):
        Human.__init__(self, id, name, age)


Ensuite avant de réaliser l’interface ReST, on va s'intéresser au format des données que l’on va manipuler. Ici pas de xml, mais du Json [json].

Dans python, pour manipuler du json, on utilise…. la librairie Json… c’est aussi simple et dans les faits la seule chose dont on va avoir besoin ce sont les méthodes “jsonify“ et “loads“.

  • La première nous servira à transformer un objet en json à la particularité que l’objet en paramètre doit être un dictionnaire ou une liste de dictionnaires. 
  • La seconde à l’inverse nous chargera notre json… pourtant comme le travail est fait à moitié, on va devoir compenser un peu et au passage se faciliter la vie.

En l'occurrence nous allons utiliser du code permettant de serialiser les informations d’un objet (avec les données des attributs, le nom de classe ou son module) et à l’inverse de deserialiser afin de recréer les instances. Ce code peut se trouver sur internet [json-conv].


def convert_to_dict(obj):
    """
    A function takes in a custom object and returns a dictionary representation of the object.
    This dict representation includes meta data such as the object's module and class names.
    """

    #  Populate the dictionary with object meta data
    obj_dict = {
        "__class__": obj.__class__.__name__,
        "__module__": obj.__module__
    }

    #  Populate the dictionary with object properties
    obj_dict.update(obj.__dict__)

    return obj_dict


def dict_to_obj(our_dict):
    """
    Function that takes in a dict and returns a custom object associated with the dict.
    This function makes use of the "__module__" and "__class__" metadata in the dictionary
    to know which object type to create.
    """
    if "__class__" in our_dict:
        # Pop ensures we remove metadata from the dict to leave only the instance arguments
        class_name = our_dict.pop("__class__")

        # Get the module name from the dict and import it
        module_name = our_dict.pop("__module__")

        # We use the built in __import__ function since the module name is not yet known at runtime
        module = __import__(module_name)

        # Get the class from the module
        class_ = getattr(module, class_name)

        # Use dictionary unpacking to initialize the object
        obj = class_(**our_dict)
    else:
        obj = our_dict
    return obj


Enfin maintenant que les éléments sont en place, il reste à mettre en oeuvre l’interface ReST. Pour cela on va utiliser le framework Flask [flask] qui permet comme avec Spring-WS (nous y reviendrons) de spécifier des routes associées aux verbes http et décorant des méthodes qui seront alors exécuté en fonction des chemins appelés.

Mais avant cela initialisons le contexte Flask: il nous faut des imports, un jeu de données (oui parce que dans cet exemple, on va pas non plus faire une BDD  pour 3 humains….) et définir le main qui construira le contexte Flask:


#!flask/bin/python
from flask import Flask, jsonify, request
import json

from json_tk import convert_to_dict, dict_to_obj
from human import Men, Women

people = [
    Men(1, "Bob", 12),
    Women(2, "Alice", 10)
]

app = Flask(__name__)

if __name__ == '__main__':
    app.run(debug=True, port=5001, host='0.0.0.0')


Maintenant on va définir les méthodes associées aux routes et verbes http.


@app.route('/humans', methods=['GET'])
def get_humans():
    return jsonify([convert_to_dict(human) for human in people])


@app.route('/humans/<int:human_id>', methods=['GET'])
def get_human(human_id):
    return jsonify([convert_to_dict(human) for human in people if human.id == human_id][0])


@app.route('/humans', methods=['POST'])
def post_human():
    print("REQUEST:" + str(request.data))
    json_response = json.loads(request.data)
    human = dict_to_obj(json_response)
    human.id = len(people)+1
    people.append(human)
    return jsonify(human.id)


On lance ca avec IntelliJ et on consulte les chemins avec un navigateur (http://0.0.0.0:5001/humans)


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }
]


Pour le test du POST, on va utiliser Postman [postman], un outil indispensable pour tester une API ReST.

On interroge alors la liste et on voit que notre nouvel humain est bien la!


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }, 
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 24, 
    "id": 3, 
    "name": "Micka"
  }
]

ReST-people-stat

On a une première application qui fournit des données de base. La seconde doit s’interfacer sur celle-ci et disons

  • fournir la liste des humains tel quel
  • fournir la moyenne des âges des humains
Il faut un outil pour interroger la première interface. Pour cela on va utiliser le framework  python_http_client qui fournit un client http.


import json
import functools
from flask import Flask, jsonify
from python_http_client import Client

from response import str_http_response
from json_tk import dict_to_obj, convert_to_dict
from human import Moyenne

client = Client(host='http://localhost:5001/humans')
app = Flask(__name__)

if __name__ == '__main__':
    app.run(debug=True, port=5002, host='0.0.0.0')

On va ajouter une classe calculant la moyenne en utilisant la librairie functools:


import functools

class Moyenne:

    def __init__(self, humans):
        self.moyenne = (functools.reduce(lambda x, y: x.age + y.age, humans)) / len(humans)


Ensuite on va d’un côté faire un recall pour voir comment fonctionne le client.

@app.route('/recall', methods=['GET'])
def get_recall():
    response = client.get()

    str_http_response(response)

    json_response = json.loads(response.body)
    humans = [dict_to_obj(human) for human in json_response]
    print("HUMANS" + str(humans))
    return jsonify([convert_to_dict(human) for human in humans])


Et on va pouvoir implementer la methode get_moy. pour  cela on aussi utiliser un module permettant une approche map/reduce. Je vous laisse analyser le code:


@app.route('/moy', methods=['GET'])
def get_moy():
    response = client.get()
    str_http_response(response)

    json_response = json.loads(response.body)
    humans = [dict_to_obj(human) for human in json_response]
    print("HUMANS" + str(humans))

    moyenne = Moyenne(humans)
    print("MOYENNE:" + str(moyenne))
    return jsonify(moyenne.__dict__)


Il ne reste plus qu’à tester! On lance l’application people. La second ensuite, on vérifie alors la methode recall:


[
  {
    "__class__": "Men", 
    "__module__": "human", 
    "age": 12, 
    "id": 1, 
    "name": "Bob"
  }, 
  {
    "__class__": "Women", 
    "__module__": "human", 
    "age": 10, 
    "id": 2, 
    "name": "Alice"
  }
]


Puis on regarde si la moyenne fonctionne:


{
  "moyenne": 11.0
}


Pour le jeux on ajoute dynamiquement un nouvel humain dans l’application people… avec Postman et on vérifie que la moyenne à changé:


{
  "moyenne": 13.0
}


Voilà nous avons fait le tour de notre application en deux temps. Pourquoi en deux parties? ba parce que ça va être un bon prétexte de mettre ça dans des conteneurs docker! Mais ça sera pour le prochain article!

Références

[rest-th] https://un-est-tout-et-tout-est-un.blogspot.com/2019/06/rest-introduction.html
[json-conv] https://school.geekwall.in/p/ByaW0IVqN/json-the-python-way
[json] https://medium.com/python-pandemonium/json-the-python-way-91aac95d4041
[flask] https://flask.palletsprojects.com/en/1.1.x/
[rest-tuto] https://www.codementor.io/sagaragarwal94/building-a-basic-restful-api-in-python-58k02xsiq
[flask-mega] https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
[postman] https://www.getpostman.com/

Aucun commentaire:

Enregistrer un commentaire