Jérôme Lafréchoux
PyConFR - 3 novembre 2019
Codage d’objets Python sous une forme adaptée pour
Processus réversible (désérialisation)
Utilisations typiques
-------- -------------
| | === dump ==> | |
| Object | | Byte stream |
| | <== load === | |
-------- -------------
-------- ------
| | === dump ==> | |
| Object | | JSON |
| | <== load === | |
-------- ------
Mais JSON ne définit que des types basiques :
JSON | Python |
---|---|
object | dict |
array | list |
string | str |
number (int/float) | int/float |
boolean | bool |
null | None |
Transforme un objet Python en dictionnaire de types simples, JSONisable
-------- ------
| | === dump ==> | |
| Object | | dict |
| | <== load === | |
-------- ------
Surcouche de json
-------- ------ ------
| | === dump ==> | | === dump ==> | |
| Object | | dict | | JSON |
| | <== load === | | <== load === | |
-------- ------ ------
-------- -------------
| | === dump ==> | |
| Object | | dict / JSON |
| | <== load & validate === | |
-------- -------------
import datetime as dt
import marshmallow as ma
class UserSchema(ma.Schema):
name = ma.fields.String()
birth_date = ma.fields.DateTime()
schema = UserSchema()
user = {"name": "Roger", "birth_date": dt.datetime(1983, 1, 23)}
schema.dump(user)
# {'name': 'Roger', 'birth_date': '1983-01-23T00:00:00'}
schema.dumps(user)
# '{"name": "Roger", "birth_date": "1983-01-23T00:00:00"}'
Modèle
Schémas
import marshmallow as ma
class TeamSchema(ma.Schema):
name = ma.fields.String()
creation_date = ma.fields.DateTime()
Ressources
Validation à la désérialisation
class MemberSchema(ma.Schema):
first_name = ma.fields.String(validate=ma.validate.Length(min=2, max=50))
last_name = ma.fields.String(required=True)
birthdate = ma.fields.DateTime()
MemberSchema().load({"first_name": "V"})
# marshmallow.exceptions.ValidationError: {
# 'last_name': ['Missing data for required field.'],
# 'first_name': ['Length must be between 2 and 50.']
# }
class MemberSchema(ma.Schema):
first_name = ma.fields.String()
last_name = ma.fields.String()
birthdate = ma.fields.DateTime()
age = ma.fields.Int(dump_only=True)
password = ma.fields.Str(load_only=True)
member = Member.get_one(last_name='Venkman')
MemberSchema().dump(member)
# {'first_name': 'Peter', 'last_name': 'Venkman', 'birthdate': '1960-09-06T00:00:00', 'age': 59}
class MemberSchema(ma.Schema):
first_name = ma.fields.String()
last_name = ma.fields.String()
birthdate = ma.fields.DateTime()
class TeamSchema(ma.Schema):
name = ma.fields.String()
members = ma.fields.List(ma.fields.Nested(MemberSchema))
team = Team.get_one(name="Ghostbusters")
TeamSchema().dumps(team)
# {'name': 'Ghostbusters',
# 'members': [
# {'first_name': 'Egon', 'last_name': 'Spengler', 'birthdate': '1958-10-02T00:00:00'},
# {'first_name': 'Peter', 'last_name': 'Venkman', 'birthdate': '1960-09-06T00:00:00'}
# ]}
pre_load
/ post_load
/ pre_dump
/ post_dump
class MemberSchema(ma.Schema):
first_name = ma.fields.String()
last_name = ma.fields.String()
birthdate = ma.fields.DateTime()
@ma.post_load
def make_instance(self, data, **kwargs):
return Member(**data)
member = MemberSchema().load(
{"first_name": "Peter", "last_name": "Venkman", "birthdate": dt.datetime(1960, 9, 6)}
)
member.first_name
# 'Peter'
ORM : Object-Relation Mapping
ODM : Object-Document Mapping
Couche d’abstraction entre objets et base de donnée
Définit le modèle avec des schémas et des champs
---------------------------
| |
---------- | -------- ▼ -------------
| | Schema | | Schema | |
| Database | <== ORM / ODM ==> | Object | <== API ==> | dict / JSON |
| | | | marshmallow | |
---------- -------- -------------
from marshmallow_mongoengine import ModelSchema
class TeamSchema(ModelSchema):
class Meta:
model = Team
class MemberSchema(ModelSchema):
class Meta:
model = Member
team = Team.objects.get(name="Ghostbusters")
TeamSchema().dump(team)
# {'id': 1,
# 'name': 'Ghostbusters',
# 'members': [
# {'first_name': 'Egon', 'last_name': 'Spengler', 'birthdate': '1958-10-02T00:00:00'},
# {'first_name': 'Peter', 'last_name': 'Venkman', 'birthdate': '1960-09-06T00:00:00'}
# ]}
TeamSchema().load({"name": "This name is too long to pass validation."})
# marshmallow.exceptions.ValidationError: {'name': ['Longer than maximum length 40.']}
Désérialise et valide les requêtes HTTP
Injecte le contenu de la requête dans la fonction de vue
from flask import Flask, request
app = Flask(__name__)
team_schema = TeamSchema()
@app.route("/teams/", methods=['POST'])
def post():
# Désérialisation et validation
try:
team_data = team_schema.load(request.json)
except ValidationError as exc:
abort(422)
# Traitement
team = Team(**team_data)
team.save()
return team_schema.dump(team), 201
Inclure les erreurs de validation dans la réponse
Prend en charge nativement les principaux serveurs web :
Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, aiohttp
Génération de la documentation OpenAPI
Introspection des schémas marshmallow
from flask import Flask, request
from marshmallow import Schema, fields
from webargs.flaskparser import use_args
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
spec = APISpec(
title="Team manager",
version="1.0.0",
openapi_version="3.0.2",
plugins=[FlaskPlugin(), MarshmallowPlugin()],
)
app = Flask(__name__)
spec.init_app(app)
@app.route("/teams/", methods=["POST"])
@use_args(TeamSchema, location="json")
def post_team():
"""Post team
---
post:
description: Add a new team.
requestBody:
description: Team
required: true
content:
application/json:
schema: TeamSchema
responses:
200:
content:
application/json:
schema: TeamSchema
"""
team = Team(**team_data)
team.save()
return TeamSchema().dump(team), 201
spec.path(view=post_team)
flask.Blueprint
→ ressourceflask.MethodView
→ GET, POST, PUT, DELETE@blp.route("/")
class Teams(MethodView):
@blp.arguments(TeamQueryArgsSchema, location="query")
@blp.response(TeamSchema(many=True))
def get(self, args):
"""List teams"""
return list(Team.query.filter_by(**args))
@blp.arguments(TeamSchema)
@blp.response(TeamSchema, code=201)
def post(self, new_team):
"""Add a new team"""
team = Team(**new_team)
db.session.add(team)
db.session.commit()
return team
@blp.route("/<uuid:team_id>")
class TeamsById(MethodView):
@blp.response(TeamSchema)
def get(self, team_id):
"""Get team by ID"""
return Team.query.get_or_404(team_id)
@blp.arguments(TeamSchema)
@blp.response(TeamSchema)
def put(self, new_team, team_id):
"""Update an existing team"""
team = Team.query.get_or_404(team_id)
TeamSchema().update(team, new_team)
db.session.add(team)
db.session.commit()
return team
@blp.response(code=204)
def delete(self, team_id):
"""Delete a team"""
team = Team.query.get_or_404(team_id)
db.session.delete(team)
db.session.commit()
page
et page_size
(query args)from .sqlcursor_pager import SQLCursorPage
@blp.route("/")
class Teams(MethodView):
@blp.arguments(TeamQueryArgsSchema, location="query")
@blp.response(TeamSchema(many=True))
@blp.paginate(SQLCursorPage)
def get(self, args):
"""List teams"""
return Team.query.filter_by(**args)
headers["X-Pagination"]
# {
# 'total': 1000, 'total_pages': 200,
# 'page': 2, 'first_page': 1, 'last_page': 200,
# 'previous_page': 1, 'next_page': 3,
# }
Identifie une version spécifique d’une ressource
GET : Économie de bande passante
If-None-Match: "686897696a7c876b7e"
304 Not Modified
PUT/DELETE : Empêche les mises à jour simultanées
If-Match: "686897696a7c876b7e"
428 Precondition required
412 Precondition failed
Actuellement, deux branches de marshmallow maintenues
Branche | Python | Date de publication |
---|---|---|
2.x | 2.7+, 3.4+ | 25 septembre 2015 |
3.x | 3.5+ | 18 août 2019 |
Institut National pour la Transition Énergétique et Environnementale du Bâtiment
Gestion énergétique de patrimoine immobilier
Planification de rénovation
BuildingEnergyManagement Server
Plateforme open-source de gestion énergétique du bâtiment
Trois bases de données
Plugin open-source de QGis
Pré-étude de faisabilité de réseaux de chaleur
Calculs asynchrones sur serveur distant via API web
Plateforme de comparaison et d’évaluation de solutions fondées sur la nature
Indicateurs socio-économiques, environnementaux, urbanisme…
Calculs synchrones sur serveur distant via API web
https://lafrech.github.io/marshmallow-pyconfr2019/