Modèles de données et OpenAPI

  • Les modèles de données pour vos services API sont créés à l'aide des dataclasses intégrées de Python.
  • Les modèles sont basés sur les résultats d'analyse de votre domaine d'activité et vous les définissez d'abord, avant que vos services ne les utilisent.
  • Les modèles permettent le typage statique et la complétion de code dans votre IDE. Si quelque chose change dans les modèles que vos services utilisent, vous recevrez immédiatement des messages dans votre IDE ou dans les outils de construction pour vous avertir que vous êtes, par exemple, en train d'accéder à un attribut d'un modèle qui n'existe plus - de cette façon, vous pouvez prévenir les erreurs d'exécution bien à l'avance, déjà pendant le développement ou les tests.
  • Les modèles peuvent être déployés à chaud sur les serveurs Zato sans aucun redémarrage.
  • Les modèles peuvent être définis en ligne, directement dans le même module Python que vos services ou dans un fichier séparé (par exemple, model.py).
  • En cours d'exécution, Zato sérialise automatiquement les modèles depuis et vers JSON, les dicts Python et OpenAPI.
  • Les modèles ne sont pas liés à REST - vous pouvez les utiliser avec n'importe quel service ou système. Par exemple, vous pouvez avoir un service WebSockets, ElasticSearch, MongoDB ou tout autre service et tous peuvent utiliser des modèles et OpenAPI.
  • Les modèles peuvent être générés à partir d'outils externes, par exemple, leur forme canonique peut être exprimée en UML et un outil externe peut les construire en Python si nécessaire.

Exemple d'utilisation

Dans l'exemple de code ci-dessous, nous supposons que nous devons représenter les besoins commerciaux suivants :

  • Un client possède plusieurs téléphones
  • Chaque téléphone a quelques attributs, tels que son numéro de carte (IMEI) ou son propriétaire actuel.
  • Nous aimerions avoir un service API qui nous dise quel téléphone possède un client donné.

Graphiquement, cela peut être exprimé comme suit :

En code, on obtiendra le résultat suivant, où chaque objet requête, réponse et modèle est une classe de données sous-classant une classe appelée Model.

# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.common.typing_ import list_
from zato.server.service import Model, Service

# ###########################################################################

@dataclass(init=False)
class Phone(Model):
    imei:       str
    owner_id:   int
    owner_name: str

# ###########################################################################

@dataclass(init=False)
class GetPhoneListRequest(Model):
    client_id: int

@dataclass(init=False)
class GetPhoneListResponse(Model):
    phone_list:    list_[Phone]
    response_type: str

# ###########################################################################

class GetPhoneDetails(Service):

    class SimpleIO:
        input  = GetPhoneListRequest
        output = GetPhoneListResponse

    def handle(self):

        # Enable type checking and type completion
        request = self.request.input # type: GetPhoneListRequest

        # Log details of our request
        self.logger.info('Processing client `%s`', request.client_id)

        # Build our response now - in a full service this information
        # would be read from an exteran system or database.

        # Our list of phones to return
        phone_list = []

        # Build the fist phone ..
        phone1 = Phone()
        phone1.imei = '123'
        phone1.owner_id = 456
        phone1.owner_name = 'John Doe'

        # .. the second one ..
        phone2 = Phone()
        phone2.imei = '789'
        phone2.owner_id = 999
        phone2.owner_name = 'Jane Doe'

        # .. populate the container for phones tha we return ..
        phone_list.append(phone1)
        phone_list.append(phone2)

        # .. build the top-level response element ..
        response = GetPhoneListResponse()
        response.response_type = 'RZH'
        response.phone_list = phone_list

        # .. and return the response to our caller
        self.response.payload = response

# ###########################################################################

Ce service est prêt à être invoqué avec :

$ curl http://pubapi:<password>@localhost:17010/zato/api/invoke/phone.get-phone-list \
    -d '{"client_id":789}'
{"response_type":"RZH",
 "phone_list": [
    {"imei":"123","owner_id":456,"owner_name":"John Doe"},
    {"imei":"789","owner_id":999,"owner_name":"Jane Doe"}]}
$

Quels types de données peuvent être utilisés

  • Lorsque vous travaillez avec des modèles, utilisez des types simples - chaînes de caractères, entiers, flottants, listes ou autres classes de données
  • N'utilisez pas de techniques avancées ou de méta-programmation comme les génériques ou les champs d'union dans les classes de données.
  • En d'autres termes, n'utilisez que ce type de types de données qui peuvent être exprimés en JSON et qui vous permettront d'exprimer 100 % de vos besoins professionnels comme le fait le JSON ordinaire.

Comment utiliser les champs facultatifs

Pour indiquer qu'un champ particulier dans un modèle est facultatif, utilisez le code suivant:

# stdlib
from dataclasses import dataclass

# Zato
from zato.common.typing_ import optional_ # Note the underscores
from zato.server.service import Model

@dataclass(init=False)
class Customer(Model):
    email: optional_[str] # This field is optional, its value may be None

Comment valider ou post-traiter les données

  • Votre modèle peut post-traiter les données qu'il reçoit après que Zato ait déjà désérialisé une requête ou une entrée vers une classe de données - cela peut être utilisé pour la validation des données reçues ou pour un post-traitement supplémentaire, par exemple, vous pouvez avoir un champ email déclaré comme une chaîne et votre modèle peut confirmer que c'est bien une chaîne mais qu'elle correspond au format email ou au domaine attendu.

  • Le post-traitement est effectué dans une méthode appelée after_created, déclarée comme suit :

    from zato.server.service import Model, ModelCtx
    
    @dataclass(init=False)
    class GetBillingListRequest(Model):
        client_id: int
        client_name: str
    
        def after_created(self, ctx:'ModelCtx') -> 'None':
            ...
    

L'objet ctx possède trois attributs d'intérêt :

  • ctx.data - un dictionnaire de données qui vient d'être traité et qui peut être post-traité
  • ctx.service - le service qui est invoqué.
  • ctx.DataClass - la classe de données que ctx.data représente.

Notez que la méthode est invoquée pour chaque JSON ou dict reçu, et non pour chaque champ individuel. Cela signifie que, si la méthode est invoquée, elle peut supposer que la validation et le traitement communs par Zato se sont déroulés avec succès, c'est-à-dire que tous les champs non facultatifs sont sûrs d'exister à ce stade.

Par exemple, si vous envoyez cette information dans une demande ..

{"client_id": 123, "client_name":"John Done"}

.. the ctx.data dict will be {'client_id': 123, 'client_name':'John Done'} rather than 'client_id' and 'client_name' individually.

Le ctx.service est le même service dont la méthode handle est invoquée. Parce que c'est un service, cela signifie qu'il a accès à tout ce qu'un service peut faire. Par exemple, pour chaque classe de données reçue, vous pouvez effectuer la validation des entrées en utilisant un appel REST externe. Vous pouvez également appliquer votre propre masquage des données, par exemple en remplaçant les numéros de carte de crédit par des signes *** . Il n'y a aucune limite à ce qu'un service peut faire.

La méthode after_created doit modifier ctx.data en place, sans rien retourner lorsqu'elle est appelée.

Où conserver les modèles

  • Les modèles peuvent être stockés dans le même fichier Python que vos services.
  • Si nécessaire, ils peuvent aussi être déplacés dans un autre fichier, par exemple model.py, et importés comme n'importe quel autre fichier Python, par exemple from model import Client.
  • Après avoir déployé à chaud un modèle, chaque service qui l'utilise sera automatiquement redéployé pour s'assurer qu'il utilise la dernière version du modèle, comme on peut l'observer dans le fichier server.log.
    INFO - Déploiement de 3 modèles depuis `/path/to/model.py` ->
      ['model.GetPhoneListRequest', 'model.GetPhoneListResponse', 'model.Phone']
    INFO - Déploiement de 1 service depuis `/path/to/phone.py` -> ['phone.get-phone-list']
    
  • Cependant, lorsque vous déployez plusieurs fichiers Python dans un groupe (par exemple, à partir de Dashboard), assurez-vous de déployer vos modèles en premier afin que les services puissent les trouver par la suite.

Dict et sérialisation JSON

Chaque modèle possède deux méthodes de commodité : .to_dict() et .to_json() qui sérialisent ce modèle vers un dict ou un JSON, respectivement.

Grâce à ces méthodes, les modèles peuvent être utilisés pour exprimer des dictionnaires complexes en utilisant des classes de données typées statiquement qui peuvent être plus faciles à créer et à maintenir.

Par exemple, lorsque vous invoquez ElasticSearch, vous pouvez être amené à construire un dict complexe pour exprimer divers filtres de requête. Au lieu de cela, créez vos propres modèles qui représentent les filtres ElasticSearch et sérialisez-les en utilisant .to_dict().

Il y a aussi de nombreuses situations où une chaîne JSON est nécessaire - construisez vos données en utilisant des modèles et appelez .to_json() au moment de la sérialisation.

Appeler d'autres services

Les modèles peuvent être utilisés lorsque vous invoquez un service Zato depuis un autre, par exemple :

# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.server.service import Model, Service

# ###########################################################################

@dataclass(init=False)
class GetClientRequest(Model):
    client_id: int

@dataclass(init=False)
class GetClientResponse(Model):
    client_name: str

# ###########################################################################

class APIService(Service):
  def handle(self):

      # The service that will be invoking
      name = 'my.api.get-client'

      # Build our request ..
      request = GetClientRequest()
      request.client_id = 123

      # .. and get client details - note that we indicate in a type hint
      # .. what the actual model we have in the response.
      response = self.invoke(name, request) # type: GetClientResponse

# ###########################################################################

class GetClient(Service)
    class SimpleIO:
      input  = GetClientRequest
      output = GetClientResponse

    def handle(self):

        # Enable type checking and type completion
        request = self.request.input # type: GetClientRequest

        # Log what we are doing ..
        self.logger.info('Returning data for %s', request.client_id)

        # .. build our response ..
        response = GetClientResponse()
        response.client_name = 'Jane Doe'

        # .. and return it to our caller.
        self.response.payload = response

# ###########################################################################

Appeler des API REST

Les modèles peuvent être utilisés pour invoquer directement les API REST, sans avoir besoin de les sérialiser en dicts ou en JSON.

# -*- coding: utf-8 -*-

# stdlib
from dataclasses import dataclass

# Zato
from zato.server.service import Model, Service

# ###########################################################################

@dataclass(init=False)
class GetClientRequest(Model):
    client_id: int

@dataclass(init=False)
class GetClientResponse(Model):
    client_name: str

# ###########################################################################

class APIService(Service):
  def handle(self):

      # The REST endpoint
      name = 'My Client API'

      # Build our request ..
      request = GetClientRequest()
      request.client_id = 123

      # .. invoke the endpoint ..
      response = self.out.rest.post(self.cid, request)

      # .. read the response data + enable type checking and type completion ..
      response = response.data # type: GetClientResponse

      # .. and log what we have received.
      self.loger.info('Data received %s', response.client_name)

# ###########################################################################

OpenAPI

  • Les définitions des services qui utilisent des modèles peuvent être exportées vers OpenAPI.

  • Notez que l'intégration avec OpenAPI est au niveau des services, qu'il y ait ou non des canaux REST directs pour eux. Cela signifie que vous pouvez avoir, par exemple, un service qui n'est accessible qu'à travers des WebSockets, mais vous pouvez toujours être capable d'y accéder à travers des clients OpenAPI, ce qui peut être utile car maintenant vous pouvez invoquer vos services à partir de n'importe quel outil compatible OpenAPI, comme Postman ci-dessous.

Exporter des services vers OpenAPI :

$ zato openapi /path/to/server \
  --include "phone*" \
  --file /tmp/test-openapi.yaml

Invoquez l'un d'entre eux dans Postman:

Spécifications API complètes

  • En plus de l'OpenAPI, il est également possible de générer des spécifications d'API complètes qui incluent également une documentation HTML.

  • Ceci est utile si vous avez besoin d'offrir une documentation statique pour votre API, en particulier si vos serveurs sont internes et que vos partenaires techniques ne peuvent pas y accéder directement - il suffit de générer une spécification d'API et de la rendre disponible en tant que site statique sans avoir besoin d'ouvrir l'accès à vos serveurs internes.

Créez une spécification d'API complète :

$ zato apispec /path/to/server \
  --include "phone*" \
  --file /tmp/test-openapi.yaml

Nous pouvons maintenant l'ouvrir dans un navigateur - notez que la définition OpenAPI est toujours là et peut être téléchargée également.

Page principale:

Détails d'un service particulier :

Sujets connexes