from jyquickhelper import add_notebook_menu
add_notebook_menu()

%matplotlib inline

"""
Explanation: Tech - API REST pour deep learning avec FastAPI
Certains sites web actuels font appels à de nombreux modèles de machine learning. Un moteur de recherche affiche beaucoup de résultats différents, des suggestions, des résultats de recherches, des recherches associées, des informations, des résultats locaux. Chacun d'entre eux fait appel à un ou plusieurs modèles de machine learning plus ou moins complexes. Le modèle classique d'un site web, ce sont deux machines :
* le client : la machine de celui qui consiste le site web
* le server : la machine qui retourne la page pour celui qui consiste le site web
De plus en plus, les pages sont dynamiques : deux internautes ne verront pas la même chose même s'ils vont au même url, ne serait-ce que pour les publicités affichées qui semblent parfois influencées par l'historique de navigation. Les pages sont calculées. Parfois, les calculs sont si complexes qu'une seule machine ne peut pas les faire seule. Le server fait appel à d'autres serveurs à qui on envoie des données pour qu'ils retournent une prédiction. Le mécanisme le plus souvent utilisé est celui-ci d'une API REST. C'est ce mécanisme qu'on va voir ici. Un avantage de ce système est que la machine qui fait des calculs peut planter sans que la machine qui produit les pages ne s'arrête. Les API REST échangent des informations le plus souvent au format JSON.
End of explanation
"""


import os

url = "https://github.com/onnx/models/raw/master/vision/classification/mobilenet/model/mobilenetv2-7.onnx"
name = url.split('/')[-1]
if not os.path.exists(name):
    print("download %r" % name)
    import urllib.request as ur
    ur.urlretrieve(url, name)

url = "https://upload.wikimedia.org/wikipedia/commons/c/c6/Okonjima_Lioness.jpg"
img_name = url.split('/')[-1]
if not os.path.exists(img_name):
    print("download %r" % img_name)
    import urllib.request as ur
    ur.urlretrieve(url, img_name)

from PIL import Image
im = Image.open(img_name)
width, height = im.width // 4, im.height // 4
im.resize((width, height))

#! pip install onnxruntime
from onnxruntime import InferenceSession
sess = InferenceSession(name)

"""
Explanation: Enoncé
Q1 : un modèle de deep learning, une image
Il faut arriver à faire marcher le code suivant.
End of explanation
"""


sess.get_inputs()[0].name, sess.get_inputs()[0].shape, sess.get_inputs()[0].type

import numpy
im224 = numpy.asarray(im.resize((224, 224)))
im224.shape

"""
Explanation: Le modèle n'accepte les images que d'une taille spéficique puis il faut la convertir en numpy.array.
End of explanation
"""


img = (im224.transpose((2, 0, 1))[numpy.newaxis, :, :, :] / 255).astype(numpy.float32)
img.shape, img.dtype

"""
Explanation: Il faut transposer, ajouter une dimension et convertir en float.
End of explanation
"""


res = sess.run(None, {'data': img})
res[0][0, :5]

"""
Explanation: On applique le modèle de deep learning, il retourne des probabilités pour chacune des mille classes sur lesquelles il a été appris.
End of explanation
"""


from ensae_teaching_cs.data import interpret_imagenet_results
cls = interpret_imagenet_results(res[0][0])
cls

"""
Explanation: Comme ce n'est pas lisible, on pourra copier le ficher imagenet_classes.py pour avoir la signification de chaque classe.
End of explanation
"""


import base64
from io import BytesIO

def image_to_str(im):
    buffered = BytesIO()
    im.save(buffered, format="JPEG")
    img_bytes = base64.b64encode(buffered.getvalue())
    img_str = img_bytes.decode('utf-8')
    return img_str

img_str = image_to_str(im)
type(img_str), len(img_str)

"""
Explanation: Ca ne marche pas trop mal.
Q2 : image et json
Les données sont échangées dans un format binaire ou texte sur internet. Il faut pouvoir tout convertir en texte. C'est étape est appelée sérialisation. C'est ce qu'on fait pour une image
End of explanation
"""


import json
data = {'image': img_str}
data_json = json.dumps(data)
type(data_json), len(data_json)

"""
Explanation: Au format JSON, cela donne quelque chose comme cela :
End of explanation
"""


def str_to_img_to_array(img_str):
    pass

"""
Explanation: Il faut écrire une fonction qui fait la manipulation inverse.
End of explanation
"""


#!pip install fastapi

%%writefile apiapp.py
# coding: utf-8
import os
import json
import base64
from io import BytesIO
import urllib.request as ur
from ensae_teaching_cs.data import interpret_imagenet_results
import numpy
from PIL import Image
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from onnxruntime import InferenceSession

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


def load_model():
    url = ("https://github.com/onnx/models/raw/master/vision/classification/"
           "mobilenet/model/mobilenetv2-7.onnx")
    name = url.split('/')[-1]
    if not os.path.exists(name):
        ur.urlretrieve(url, name)
    return InferenceSession(name)


sess = load_model()


def image_to_classification(data_json, sess):
    # on convertit le json en image
    # ...
    # on convertit l'image en array et on donne les dimensions attendues (224, 224)
    # ...
    # on prédit
    # ...
    # on retourne les 10 premières classes
    # return ...
    return []


# Defining a custom object using Pydantic
class Item(BaseModel):
    item_id: str
    image: str


@app.post("/imgpred/{model}/")
def read_item(model: str, item: Item):
    if model == 'mobilenetv2':
        data_json = item.image
        try:
            cls = image_to_classification(data_json, sess)
        except Exception as e:
            raise HTTPException(
                status_code=500, detail="Cannot predict due to %r" % e)
        return {"item_id": item.item_id,
                "results": [(name, float(p)) for name, p in cls.items()]}
    raise HTTPException(status_code=404, detail="Unexpected model %r." % model)

"""
Explanation: Q3 : FastAPI
On s'inspire de l'exemple suivant : FastAPI qu'on adapte pour donner l'exemple qui suit qu'il faut compléter pour retourner les résultats de classification.
End of explanation
"""


# !python -m uvicorn apiapp:app

"""
Explanation: Ensuite, il faut exécuter la ligne de commande.
End of explanation
"""


# !pip install requests

"""
Explanation: Puis ouvrir l'url : http://127.0.0.1:8000. Normalement, cela affiche {"Hello":"World"}.
Puis l'url http://127.0.0.1:8000/docs. C'est cette page de documentation que génère automatiquement FastAPI et c'est très pratique pour tester l'API depuis un navigateur.
Q4 : il faut tester l'application depuis Python maintenant
Avec le module requests et faire une requête de type POST.
End of explanation
"""


def str_to_img_to_array(img_str):
    # str -> bytes
    img_bytes = img_str.encode('utf-8')
    # des bytes restreint aux bytes libérés
    enc = base64.b64decode(img_bytes)
    # des bytes libérés à l'image
    buf = BytesIO(enc)
    return Image.open(buf)

str_to_img_to_array(img_str).resize((224,224))

"""
Explanation: Q5 : que manque-t-il pour faire une vraie API ?
Réponses
Q1
Un peu de persévérance n'a jamais fait de mal.
Q2 : json
End of explanation
"""


def image_to_classification(img_str, sess):
    # on convertit le json en image
    im = str_to_img_to_array(img_str)
    # on convertit l'image en array et on donne les dimensions attendues
    img = numpy.asarray(im.resize((224, 224)))
    im224 = numpy.asarray(im.resize((224, 224)))
    img = (im224.transpose((2, 0, 1))[numpy.newaxis, :, :, :] / 255).astype(numpy.float32)
    # on prédit
    res = sess.run(None, {'data': img})
    # on retourne les 10 premières classes
    return interpret_imagenet_results(res[0][0])

image_to_classification(img_str, sess)

"""
Explanation: Même fonction qui retourne les résultats de classification.
End of explanation
"""


%%writefile apiapp.py
# coding: utf-8
import os
import json
import base64
from io import BytesIO
import urllib.request as ur
from imagenet_classes import interpret_imagenet_results
import numpy
from PIL import Image
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from onnxruntime import InferenceSession


def load_model():
    url = ("https://github.com/onnx/models/raw/master/vision/classification/"
           "mobilenet/model/mobilenetv2-7.onnx")
    name = url.split('/')[-1]
    if not os.path.exists(name):
        ur.urlretrieve(url, name)
    return InferenceSession(name)


def str_to_img_to_array(img):
    # str -> bytes
    img_bytes = img.encode('utf-8')
    # des bytes restreint aux bytes libérés
    enc = base64.b64decode(img_bytes)
    # des bytes libérés à l'image
    buf = BytesIO(enc)
    return Image.open(buf)


def image_to_classification(img_str, sess):
    # on convertit le json en image
    im = str_to_img_to_array(img_str)
    # on convertit l'image en array et on donne les dimensions attendues
    img = numpy.asarray(im.resize((224, 224)))
    im224 = numpy.asarray(im.resize((224, 224)))
    img = (im224.transpose((2, 0, 1))[numpy.newaxis, :, :, :] / 255).astype(numpy.float32)
    # on prédit
    res = sess.run(None, {'data': img})
    # on retourne les 10 premières classes
    return interpret_imagenet_results(res[0][0])


sess = load_model()
app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


# Defining a custom object using Pydantic
class Item(BaseModel):
    item_id: str
    image: str


@app.post("/imgpred/{model}/")
def read_item(model: str, item: Item):
    if model == 'mobilenetv2':
        img_str = item.image
        try:
            cls = image_to_classification(img_str, sess)
        except Exception as e:
            raise HTTPException(
                status_code=500, detail="Cannot predict due to %r" % e)
        return {"item_id": item.item_id,
                "results": [(name, float(p)) for name, p in cls.items()]}
    raise HTTPException(status_code=404, detail="Unexpected model %r." % model)

"""
Explanation: Q3 : fichier modifié
End of explanation
"""


import requests
class ReqError:
    def __init__(self, e):
        self.e = e
    @property
    def text(self):
        return str(self.e)
    def json(self):
        return str(self.e)
try:
    r = requests.get('http://127.0.0.1:8000/')
except Exception as e:
    r = ReqError(e)
print(r, r.text)

from IPython.display import HTML
try:
    r = requests.get('http://127.0.0.1:8000/docs')
except Exception as e:
    r = ReqError(e)
print(r, r.text)

def query_data(img_id, img):
    width, height = img.width // 8, img.height // 8
    img = img.resize((width, height))
    buffered = BytesIO()
    img.save(buffered, format="JPEG")
    img_bytes = base64.b64encode(buffered.getvalue())
    img_str = img_bytes.decode('utf-8')
    return {'item_id': img_id, 'image': img_str}


def query(img_id, img):
    data = query_data(img_id, img)
    try:
        return requests.post('http://127.0.0.1:8000/imgpred/mobilenetv2/',
                             data=json.dumps(data))
    except Exception as e:
        return ReqError(e)


res = query(img_name, Image.open(img_name))
res.json()

# !curl -X POST "http://127.0.0.1:8000/imgpred/mobilenetv2/" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"item_id\":\"Okonjima_Lioness.jpg\",\"image\":\"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\"}"

"""
Explanation: Ensuite il faut exécuter la ligne de commande : python -m uvicorn apiapp:app.
Puis aller à l'adresse suivante : http://127.0.0.1:8000/.
Le message {"Hello": "World"} doit apparaître sur l'écran.
Ensuite, il faut aller à l'adresse : http://127.0.0.1:8000/docs qui crée automatiquement une documentation générée pour ce site web en local.
Q4 : utiliser l'API REST
Python propose des outils pour utiliser l'API REST.
End of explanation
"""


