Micro service avec Python (présentation)

1.Tout d'abord

Récemment, j'ai eu l'occasion d'étudier le développement de microservices (appelés API REST) en utilisant Lightweight Language (LL). Il existe différents langages tels que Ruby, Go, Python, etc. même s'il s'appelle LL, mais comme de nombreuses connaissances utilisent Python dans le domaine de l'apprentissage automatique et de l'IoT, cette fois sur la façon d'implémenter des microservices à l'aide de Python Je voudrais le garder en mémoire. C'est la première fois que je lis Python, j'espère donc que vous le lirez dans cet esprit.

1.1 Environnement de vérification

L'environnement utilisé dans cette vérification est le suivant. Je n'ai pas essayé l'environnement virtuel Python cette fois. (Car on dit que ça ne marche pas sous Windows)

1.2 Bibliothèque à utiliser

Cette fois, il s'agit d'une bibliothèque utilisée pour créer un service de macro en Python. Toutes sont de simples bibliothèques faciles à utiliser et qui peuvent toutes être installées avec "pip install".

Numéro d'article Bibliothèque Aperçu site
1 flask Cadre mvc léger http://flask.pocoo.org/
2 peewee O/Cartographie R http://docs.peewee-orm.com/en/latest/
3 psycopg2 Requis pour l'accès PostgreSQL https://pypi.python.org/pypi/psycopg2
4 cerberus Bibliothèque de contrôle d'entrée http://docs.python-cerberus.org/en/stable/
5 requests Client HTTP http://docs.python-requests.org/en/master/

2 Construction de l'environnement

2.1 Installer Python

Je l'ai installé selon l'installateur. La destination d'installation est la valeur par défaut C: \ Python27.

2.2 Réussir

Les deux emplacements suivants ont été ajoutés à la variable d'environnement path. (Pour référence, le délimiteur path dans Windows est;)

2.3 Définition des variables d'environnement pour dépasser Proxy

Si Proxy est requis pour l'accès Internet en raison de la configuration réseau de l'organisation, définissez la variable d'environnement pour dépasser Proxy.

Il peut s'agir d'une variable d'environnement utilisateur ou d'une variable d'environnement système. Si vous souhaitez le définir uniquement lors de l'installation de pip, nous vous recommandons de le définir temporairement avec set.

set HTTPS_PROXY=userid:[email protected]:8080
set HTTP_PROXY=userid:[email protected]:8080

Faites correspondre l'hôte proxy, le numéro de port, l'ID utilisateur et le mot de passe à votre environnement. Si Proxy ne nécessite pas d'authentification de l'utilisateur, il n'a pas besoin de précéder «@».

2.4 Définissez l'URL que vous ne souhaitez pas passer par Proxy en tant que variable d'environnement

Il peut y avoir des URL que vous ne souhaitez pas utiliser Proxy, par exemple lorsque vous souhaitez accéder à un serveur sur un réseau privé. Dans ce cas, utilisez la variable d'environnement NO_PROXY.

set NO_PROXY=127.0.0.1;localhost;apserver

Lorsque vous essayez d'accéder à un serveur préparé par vous-même à l'aide de requests Pour une raison quelconque, je ne pouvais pas y accéder via Proxy, j'ai donc décidé de définir la variable d'environnement NO_PROXY.

2.5 Installer les bibliothèques requises avec pip

Encore une fois, je n'ai pas essayé l'environnement virtuel Python cette fois. Alors je l'ai installé directement avec pip.

pip install peewee
pip install psycopg2
pip install requests
pip install cerberus
pip install flask

3 concept d'application

Implémentons une application qui introduit l'utilisation minimale des bibliothèques (flask, cerberus, peewee) introduites cette fois.

Cette fois, nous ne considérerons pas la division des fonctions ou la division des fichiers selon la responsabilité. Pour une meilleure visibilité, nous utiliserons un code source pour l'application Web du microservice et un code source pour le client qui utilise le microservice.

3.1 Aperçu de l'application concept

Il s'agit d'une application de microservice (ou API REST) qui possède une API qui ajoute uniquement des enregistrements à la table question ci-dessous.

create_table.sql


DROP TABLE IF EXISTS question;

CREATE TABLE question (
    question_code   VARCHAR(10)    NOT NULL,
    category        VARCHAR(10)    NOT NULL,
    message         VARCHAR(100)   NOT NULL,
    CONSTRAINT question_pk PRIMARY KEY(question_code)
);

Les spécifications et restrictions de l'application sont les suivantes. Il semble que les spécifications aient des objectifs mixtes, mais voici les points de mise en œuvre.

3.2 Microservice de démonstration

3.2.1 Code source

demoapp.py


# -*- coding: utf-8 -*-
import os
from flask import Flask, abort, request, make_response, jsonify
import peewee
from playhouse.pool import PooledPostgresqlExtDatabase
import cerberus

# peewee
db = PooledPostgresqlExtDatabase(
    database = os.getenv("APP_DB_DATABASE", "demodb"),
    host = os.getenv("APP_DB_HOST", "localhost"),
    port = os.getenv("APP_DB_PORT", 5432),
    user = os.getenv("APP_DB_USER", "postgres"),
    password = os.getenv("APP_DB_PASSWORD", "postgres"),
    max_connections = os.getenv("APP_DB_CONNECTIONS", 4),
    stale_timeout = os.getenv("APP_DB_STALE_TIMEOUT", 300),
    register_hstore = False)

class BaseModel(peewee.Model):
    class Meta:
        database = db

# model
class Question(BaseModel):
    question_code = peewee.CharField(primary_key=True)
    category = peewee.CharField()
    message = peewee.CharField()

# validation schema for cerberus
question_schema = {
    'question_code' : {
        'type' : 'string',
        'required' : True,
        'empty' : False,
        'maxlength' : 10,
        'regex' : '^[0-9]+$'
    },
    'category' : {
        'type' : 'string',
        'required' : True,
        'empty' : False,
        'maxlength' : 10
    },
    'message' : {
        'type' : 'string',
        'required' : True,
        'empty' : False,
        'maxlength' : 100
    }
}

# flask
app = Flask(__name__)

# rest api
@app.route('/questions', methods=['POST'])
def register_question():
    #Vérifiez l'entrée
    v = cerberus.Validator(question_schema)
    v.allow_unknown = True
    validate_pass = v.validate(request.json)
    if not validate_pass:
        abort(404)
    
    #Appeler la logique métier
    result = register(request.json)
    #Retour du résultat du traitement (format JSON)
    return make_response(jsonify(result))

# error handling
@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify({'error' : 'Not Found'}), 404)

@app.errorhandler(500)
def server_error(error):
    return make_response(jsonify({'error' : 'ERROR'}), 500)

#Logique métier
@db.atomic()
def register(input):
    # create instance
    question = Question()
    question.question_code = input.get("question_code")
    question.category = input.get("category")
    question.message = input.get("message")
    # insert record using peewee api
    question.save(force_insert=True)
    result = {
        'result' : True,
        'content' : question.__dict__['_data']
    }
    return result

# main
if __name__ == "__main__":
    app.run(host=os.getenv("APP_ADDRESS", 'localhost'), \
    port=os.getenv("APP_PORT", 3000))

3.2.2 Réglage de la base de données dans peewee

3.2.2.1 DB et pool de connexions correspondants

Avec peewee, vous pouvez accéder à des bases de données telles que SQL Lite, MySQL et PostgreSQL. Voir http://docs.peewee-orm.com/en/latest/peewee/database.html pour plus d'informations.

Puisque les micro-services sont bien sûr des applications web, je pense que la fonction de pool de connexion sera essentielle. Bien sûr, peewee prend également en charge les pools de connexions. Les paramètres sont différents pour chaque base de données, et dans le cas de PostgreSQL, utilisez la classe PooledPostgresqlExtDatabase.

Lors de l'utilisation de la classe PooledPostgresqlExtDatabase, une erreur se produira lors de l'exécution si psycopg2 n'est pas installé.

Bien que cela n'ait rien à voir avec la fonction peewee, il est recommandé que les informations de connexion à la base de données (hôte, numéro de port, ID utilisateur, mot de passe, etc.) soient définies de l'extérieur du programme (variables d'environnement) car elles dépendent de l'environnement.

3.2.2.2 Fonction HSTORE de PostgreSQL

Lors de l'utilisation de PostgreSQL avec peewee, on suppose que la fonction appelée HSTORE de PostgreSQL est utilisée par défaut. Par conséquent, si HSTORE n'est pas activé dans la base de données à utiliser, une erreur se produira dans l'accès à la base de données de peewee.

En guise de contre-mesure, activez HSTORE avec CREATE EXTENSION hstore; dans la base de données, Vous devrez également définir register_hstore = False pour empêcher peewee d'utiliser HSTORE.

3.2.2.3 Définition du modèle de base

Afin d'utiliser la fonction de mappage O / R de peewee, définissez une classe qui hérite du Model de peewee. Cette fois, il s'agit d'une classe appelée BaseModel, mais cette classe doit définir la classe Meta en interne et définir l'objet de définition de base de données mentionné ci-dessus dans le champ database.

3.2.3 Définition du modèle peewee

La fonction de mappage O / R de peewee associe des tables à des classes et des colonnes à des champs. Ensuite, l'objet modèle de peewee est mappé à un enregistrement de la table.

Le modèle peewee est défini comme une classe qui hérite du BaseModel mentionné ci-dessus. Le champ définit le type de champ peewee en fonction du type de données de la table.

Voir http://docs.peewee-orm.com/en/latest/peewee/models.html#fields pour plus d'informations.

Définissez primary_key = True dans le champ de clé primaire. Si vous ne définissez pas primary_key, peewee part du principe qu'il existe une colonne de clé primaire appelée id. Par conséquent, bien sûr, si l'ID de colonne n'existe pas dans la table, une erreur se produira.

Si le nom du champ et le nom de la colonne sont différents, définissez explicitement la colonne avec db_column = column name.

3.2.4 Requête de cartographie par flacon

Pour créer une application flask, créez d'abord une instance de flask avec __name__ comme argument. Ensuite, utilisez le décorateur de fonction route () pour associer la requête HTTP à la fonction en définissant le mappage de requête par flask dans la fonction définie.

Pour ceux qui ont de l'expérience avec Spring Framework (Spring MVC) en Java, il peut être plus facile de penser à l'annotation @ RequestMapping. (Flask n'a pas de fonction de mappage dans les paramètres de requête ou les en-têtes)

@app.route('/questions/<string:question_code>', methods=['GET'])
def reference_question(question_code):

Voir http://flask.pocoo.org/docs/0.12/quickstart/#routing pour plus d'informations sur le mappage des demandes.

3.2.5 Traitement des erreurs par flacon

falsk fournit un décorateur de fonction ʻerrorhandler () `pour définir la gestion des erreurs. Définissez le code d'état HTTP à gérer dans l'argument. L'application de démonstration gère désormais à la fois 404 et 500.

3.2.6 Contrôle des entrées par cerberus

3.2.6.1 Définition du schéma de règle de contrôle d'entrée

Dans cerberus, les règles de vérification des entrées sont appelées schémas. (Peut-être que cela détermine la structure des données, donc je pense que cela s'appelle un schéma comme DB)

Le schéma est défini par le type de dictionnaire Python. Je ne connaissais pas le type de dictionnaire Python, donc quand je l'ai vu pour la première fois, j'ai pensé que je l'écrirais en JSON.

Décrivez le type de données («type»), requis / non requis («requis») et les règles de vérification d'entrée à appliquer («maxlength», «regex», etc.) pour chaque champ. Je pense que c'est assez simple pour avoir du sens en un coup d'œil.

Voir http://docs.python-cerberus.org/en/stable/validation-rules.html pour les règles de vérification d'entrée fournies par défaut.

3.2.6.2 Exécution du contrôle d'entrée

La vérification d'entrée par cerberus crée d'abord une instance de Validator selon le schéma, et utilise cette instance pour effectuer la vérification d'entrée. (Il existe également une méthode à utiliser sans créer d'instance pour chaque schéma, mais je vais l'omettre cette fois)

Dans l'application de démonstration, nous avons défini un schéma appelé question_schema, nous avons donc créé une instance de Validator avec ceci comme argument.

L'exécution de la vérification d'entrée est facile, et la méthode validate est exécutée avec les données de type dictionnaire qui sont la cible de la vérification d'entrée comme argument. En tant que valeur de retour, «True» est retourné s'il n'y a pas d'erreur dans la vérification d'entrée, et «False» est retourné s'il y a une erreur.

Lors de l'utilisation en coopération avec flask, les données de la requête sont accessibles au format dictionnaire (format JSON) avec la propriété request.json, définissez-la comme argument de la méthode validate. Puisqu'il s'agit d'une application REST, elle ne sera pas utilisée cette fois, mais les données du formulaire d'entrée sont accessibles avec request.form.

validate_pass = v.validate(request.json)
validate_pass = v.validate(request.form)

Si vous voulez juste vérifier l'instance indépendamment de flask, utilisez __dict__ car la méthode validate ne peut prendre que des arguments de type dictionnaire.

question_ng = Question()
question_ng.question_code = "abc0123456789xyz"
question_ng.category = None
question_ng.message = ""

validate_pass = v.validate(question_ng.__dict__)

Par défaut, cerberus entraînera une erreur de vérification d'entrée si les données d'entrée contiennent des champs qui ne sont pas définis dans le schéma. Pour changer ce comportement, définissez la propriété ʻallow_unknown de l'instance du validateur sur True. (ʻAllow_unknown = True)

Comme vous pouvez le voir dans «À propos des détails des erreurs de vérification d'entrée» ci-dessous, ** les instances de cerberus Validator sont des instances avec état qui conservent leur état en interne. Pas apatride. ** **

C'est pourquoi l'application de démonstration crée une instance Validator pour chaque demande. Il faut envisager de rationaliser (réduire) le processus d'instanciation et le validateur thread-safe.

3.2.6.3 À propos du contenu d'erreur de l'erreur de contrôle d'entrée

Dans cerberus, le contenu d'erreur de l'erreur de vérification d'entrée est stocké dans la propriété ʻerrors` de l'instance de Validator. Il est conservé dans les propriétés de l'instance, donc bien sûr, il sera mis à jour à chaque fois que vous le vérifiez. Vous savez maintenant que vous ne pouvez pas partager une instance de Validator avec plusieurs threads (ce n'est pas thread-safe).

if not validate_pass:
    print v.errors
    abort(404)

Afin de vérifier le contenu de la propriété des erreurs, essayez de vérifier les données d'entrée qui provoquent l'erreur suivante.

Données d'entrée qui provoquent une erreur


question = {
    'question_code' : '99999999990000000',
    'category' : '',
    'message' : 'hello'
}

Le contenu des erreurs est un type de dictionnaire dans lequel le champ qui a causé l'erreur est la clé et le tableau des messages d'erreur est la valeur. Par conséquent, si plusieurs erreurs de vérification d'entrée sont appliquées à un champ, plusieurs messages d'erreur seront stockés.

V affiché.Contenu des erreurs


{u'category': ['empty values not allowed'], u'question_code': ['max length is 10']}

3.2.7 Contrôle des transactions dans peewee

peewee fournit un décorateur de fonctions pour le contrôle des transactions. Vous pouvez définir des limites de transaction simplement en ajoutant un décorateur ʻatomic () à la fonction. Cela ressemble à une transaction déclarative utilisant @ Transactional` de Spring Framework, donc je pense que cela sera familier à ceux qui ont utilisé Spring. (Le mécanisme est différent car il s'agit de la spécification du langage de développement et de l'AOP)

Bien sûr, vous pouvez également imbriquer les transactions et contrôler explicitement le début, la restauration et la validation. Voir http://docs.peewee-orm.com/en/latest/peewee/transactions.html pour plus d'informations sur le contrôle des transactions.

3.2.8 insérer dans peewee

Il y a deux façons d'insérer dans peewee. Par conséquent, lors du développement avec plusieurs membres, il semble nécessaire d'organiser les méthodes.

3.2.8.1 Enregistrement d'enregistrement et instanciation en même temps

Utilisez la méthode create () de la classe model pour enregistrer des enregistrements et créer des instances en même temps. Définissez les données requises pour l'insertion (toutes les données de la colonne Not Null) comme argument de la méthode create.

question = Question.create(question_code = input.get("question_code"), \
    category = input.get("category"), \
    message = input.get("message"))

L'application de démonstration n'utilise pas cette méthode car elle ne semble pas correspondre à la fonction de mappage O / R des instances et des enregistrements de mappage.

Voir http://docs.peewee-orm.com/en/latest/peewee/api.html#Model.create pour plus d'informations.

3.2.8.2 Enregistrer les enregistrements à tout moment après la création d'une instance

Après avoir créé l'instance que vous souhaitez enregistrer, enregistrez l'enregistrement en exécutant la méthode save () de l'instance à tout moment. À ce stade, le point est de définir «True» sur la propriété «force_insert» de l'argument. (Force_insert = True)

À l'origine, la méthode save () est pour la mise à jour (émission de mise à jour), mais en définissant force_insert = True, l'insertion sera émise.

Voir http://docs.peewee-orm.com/en/latest/peewee/api.html#Model.save pour plus d'informations.

3.3 Client de démonstration

3.3.1 Code source

democlient.py


# -*- coding: utf-8 -*-
import requests

# if proxy pass error occurred, try "set NO_PROXY=127.0.0.1;localhost" at console
def register_question():
    print("[POST] /questions")
    
    question = {
        'question_code' : '9999999999',
        'category' : 'demo',
        'message' : 'hello'
    }
    headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
    response = requests.post('http://localhost:3000/questions',
        json=question, headers=headers)
    print(response.status_code)
    print(response.content)

# main
if __name__ == "__main__":
    register_question()

3.3.2 Envoi de requêtes HTTP via des requêtes

Les requêtes ont des méthodes qui correspondent aux méthodes HTTP telles que «post» et «get». Cette fois, j'ai utilisé «post» pour envoyer la demande par POST.

Lors de l'envoi de données au format json, définissez les données dans l'argument json comme json = question. De même, si vous souhaitez spécifier l'en-tête HTTP au moment de la requête, définissez-le dans l'argument headers.

En fait, lorsque l'argument json est utilisé, ʻapplication / json est automatiquement défini dans Content-type`. Vous n'avez donc pas vraiment besoin de le définir explicitement comme le code source de la démo. (Je voulais savoir comment définir l'en-tête HTTP, je l'ai donc défini cette fois)

3.4 Contrôle de fonctionnement

3.4.1 Démarrage des microservices

C:\tmp>python demoapp.py
 * Running on http://localhost:3000/ (Press CTRL+C to quit)

Lors de la mise à l'échelle, plusieurs processus peuvent s'exécuter sur le même ordinateur. A ce moment, il est nécessaire de changer pour que le port d'écoute ne soit pas couvert. L'application de démonstration vous permet d'écraser le port d'écoute à partir d'une variable d'environnement, afin que vous puissiez le modifier au démarrage.

C:\tmp>set APP_PORT=3001
C:\tmp>python demoapp.py
 * Running on http://localhost:3001/ (Press CTRL+C to quit)

3.4.2 Lancement du client de démonstration

Exécutez le client de démonstration sans les données pertinentes dans la base de données. Je pense qu'il sera exécuté normalement et que les données enregistrées seront renvoyées au format JSON.

C:\tmp>python democlient.py
[POST] /questions
200
{
  "content": {
    "category": "demo",
    "message": "hello",
    "question_code": "9999999999"
  },
  "result": true
}

Ensuite, essayez à nouveau d'exécuter le client de démonstration dans cet état. Bien sûr, vous obtiendrez une erreur car vous serez pris dans une contrainte unique. Si l'état HTTP de «{'error': 'ERROR'} 'est renvoyé, la gestion des erreurs définie fonctionne également.

C:\tmp>python democlient.py
[POST] /questions
500
{
  "error": "ERROR"
}

Enfin, supprimez les données pertinentes de la base de données et réessayez. Cela devrait fonctionner correctement car l'erreur de contrainte unique ne se produit plus.

C:\tmp>python democlient.py
[POST] /questions
200
{
  "content": {
    "category": "demo",
    "message": "hello",
    "question_code": "9999999999"
  },
  "result": true
}

4 Enfin

J'ai expliqué comment créer un micro service avec Python en utilisant flask, cerberus et peewee. Je pense que j'ai été en mesure de présenter à quel point il est facile d'implémenter les fonctions de gestion des requêtes HTTP, de vérification des entrées et d'accès à la base de données qui sont le minimum requis pour une application Web en utilisant la bibliothèque introduite cette fois.

Comme je l'ai expliqué au début de l'application concept, nous ne considérons pas la division des fonctions et la division des fichiers en fonction des responsabilités qui sont importantes dans le développement réel du système. En outre, il est nécessaire de considérer séparément la certification, l'autorisation, le contrôle de flux, la surveillance, l'acquisition de journaux, etc., qui sont essentiels pour divulguer les micro-services à l'extérieur.

5 Informations de référence

Fiche technique de notation Markdown Note d'écriture de Markdown Implémentation rapide de l'API REST en Python Essayez de travailler avec la base de données en utilisant Peewee de l'ORM de Python

Recommended Posts

Micro service avec Python (présentation)
Python: prétraitement dans l'apprentissage automatique: présentation
Quadtree en Python --2
CURL en Python
Métaprogrammation avec Python
Python 3.3 avec Anaconda
Géocodage en python
SendKeys en Python
Méta-analyse en Python
Unittest en Python
Discord en Python
DCI en Python
tri rapide en python
nCr en python
N-Gram en Python
Programmation avec Python
Plink en Python
Constante en Python
Sqlite en Python
Étape AIC en Python
LINE-Bot [0] en Python
CSV en Python
Assemblage inversé avec Python
Réflexion en Python
Constante en Python
nCr en Python.
format en python
Scons en Python 3
Puyopuyo en python
python dans virtualenv
PPAP en Python
Quad-tree en Python
Réflexion en Python
Chimie avec Python
Hashable en Python
DirectLiNGAM en Python
LiNGAM en Python
Aplatir en Python
Aplatir en python
Liste triée en Python
AtCoder # 36 quotidien avec Python
Texte de cluster en Python
AtCoder # 2 tous les jours avec Python
Daily AtCoder # 32 en Python
Daily AtCoder # 6 en Python
Modifier les polices en Python
Motif singleton en Python
Opérations sur les fichiers en Python
Lire DXF avec python
Daily AtCoder # 53 en Python
Séquence de touches en Python
Utilisez config.ini avec Python
Daily AtCoder # 33 en Python
Résoudre ABC168D en Python
Distribution logistique en Python
AtCoder # 7 tous les jours avec Python
Une doublure en Python
GRPC simple en Python