Flask Rest json with Marshmallow

Models

Person

from dataclasses import dataclass
from typing import List
 
from flask_example import db
from models.Address import Address
 
 
@dataclass
class Person(db.Model):
    id: int = db.Column(db.Integer, primary_key=True)
    firstname: str = db.Column(db.String)
    lastname = db.Column(db.String)
    addresses = db.relationship('Address',
                                back_populates='person',
                                lazy='select',
                                cascade="all, delete-orphan")
 
    # This constructor is not mandatory, but to have a check about parameter passed if we want to 
    # create  manually a person instance, it is better to define it
    # Beware: the parameters must be compliant with the marshmallow schema.
    def __init__(self, firstname: str, lastname: str, id: int = None, addresses: List[str] = None):
        print(f'Person CONSTRUCTOR')
        self.id = id
        self.lastname = lastname
        self.firstname = firstname
        if not addresses:
            addresses = []
        self.addresses = addresses
 
    def add_address(self, address: Address):
        self.addresses.append(address)
 
    def __repr__(self) -> str:
        return f'Person: id={self.id}, firstname={self.firstname}, lastname={self.lastname},' \
            # f'addresses={self.addresses}'

Address

from dataclasses import dataclass
 
from flask_example import db
 
 
@dataclass
class Address(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    country_code: str = db.Column(db.String)
    zip_code = db.Column(db.String)
    street = db.Column(db.String)
    person = db.relationship('Person', back_populates='addresses')
 
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
 
    def __init__(self, country_code: str, zip_code: str, street: str):
        print(f'Address CONSTRUCTOR')
        self.zip_code = zip_code
        self.country_code = country_code
        self.street = street
 
    def __repr__(self) -> str:
        return f'Address: id={self.id}, country_code={self.country_code}, zip_code={self.zip_code}, ' \
               f'street={self.street}'

Marshmallow schemas: SQLAlchemyAutoSchema

Options class for SQLAlchemyAutoSchema:
The same options as SQLAlchemySchemaOpts, with the addition of:
include_fk: Whether to include foreign fields; defaults to False.
include_relationships: Whether to include relationships; defaults to False

Remarks on our configuration:
– Nested field allows to serialize/deserialize the relationship.
Because addresses is a one-to-many relationship, we need to specify many=true.
– These are default configurations to serialize/deserialize. We can override it at runtime for each usage.

from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from marshmallow_sqlalchemy import fields
 
from models.Address import Address
from models.Person import Person
 
 
class AddressSchema(SQLAlchemyAutoSchema):
    class Meta:
        model = Address
        include_fk = True
        load_instance = True
 
 
class PersonSchema(SQLAlchemyAutoSchema):
    class Meta:
        model = Person
        include_relationships = True
        load_instance = True
 
    addresses = fields.Nested(AddressSchema, many=True)

REST json routes

Add a person and optionally his addresses

Some remarks:
– The person_id field of address is required according to the model, so we need to ignore this check during the deserialization.
SQLAlchemyAutoSchema.load() needs either a session or to specify we want to work with a transient model.
Here we specify we want a transient model, because it spares some SQL queries.

@app.route("/api/person", methods=['POST'])
def add_person():
    json_dic: Dict = request.json
    person_controller_logger.info(f'json_dic input={json_dic}, type={type(json_dic)}')
    # json_dic input={'lastname': 'David', 'firstname': 'boyoDoe', 'addresses': [{'street': '8 oxford 
    # strict', 'country_code': 'FR', 'zip_code': '75001'}]}, type=<class 'dict'>
    person_schema: PersonSchema = PersonSchema(exclude=['addresses.person_id'])
    person = person_schema.load(json_dic, transient=True)
    person_controller_logger.info(f'person converted: {person}')
    person_repository.save(person)
    return jsonify({
            "id": person.id
    })

Update a person and replace his addresses

Some remarks:
– Because we want to override existing addresses, we don’t want to deserialize the addresses.person_id.

@app.route("/api/person/<int:person_id>", methods=['PUT'])
def update_person(person_id: int):
    person_controller_logger.info(f'update_person() with person_id={person_id}')
    json_dic: Dict = request.json
    person_controller_logger.info(f'json_dic input={json_dic}, type={type(json_dic)}')
    person_schema: PersonSchema = PersonSchema(exclude=['addresses.person_id'])
    person = person_schema.load(json_dic, transient=True)
    person_controller_logger.info(f'person converted: {person}')
    person_repository.save(person)
    return jsonify({
            "id": person.id
    })

Get all persons with optional filters

Some remarks:
request.args is a special dictionary type: werkzeug.datastructures.ImmutableMultiDict that contains url query params.
many=True: because we want to serialize multiple person instances.
exclude=['addresses']: because we don’t want to retrieve addresses.
Without this attribute, SQL queries would be performed to retrieve addresses.

@app.route('/api/persons', methods=['GET'])
def get_persons():
    print(f'request.args={request.args}, type={type(request.args)}')
    country_code: str = request.args.get('country_code', None)
    lastname: str = request.args.get('lastname', None)
    person_controller_logger.info(f'get_persons() with lastname={lastname},'
                                  f'country_code={country_code}')
    persons: list[Person] = person_repository.find_all(lastname=lastname, country_code=country_code)
    person_schema: PersonSchema = PersonSchema(many=True, exclude=['addresses'])
    person_json: Dict = person_schema.dump(persons)
    return person_json

Find person by id with optional fetch addresses

Some remarks:
– If the person is not found in the database, we call: abort() that stops the execution of the function and raises a werkzeug.exceptions.HTTPException for the given status code.
To convert the exception into a REST json response, we can define an errorhandler that maps HTTPExceptions to json responses.

@app.route('/api/person/<int:person_id>', methods=['GET'])
def get_person(person_id: int):
    print(f'request.args={request.args}, type={type(request.args)}')
    # request.args=ImmutableMultiDict([('fetch_address', '')]), type=<class 
    # 'werkzeug.datastructures.ImmutableMultiDict'>
    fetch_address: bool = True if 'fetch_address' in request.args else False
    person_controller_logger.info(f'person_id input={person_id},fetch_address={fetch_address}')
    person: Person = person_repository.find_by_id(person_id, fetch_address)
    if not person:
        abort(404)
    print(f'person={person}, type={type(person)}')
    # If the object is not existing, the process is aborted here and the function returns 404 response
    person_schema: PersonSchema = \
        PersonSchema() if fetch_address else PersonSchema(exclude=['addresses'])
    person_json: Dict = person_schema.dump(person)
    return person_json

Errorhandler example:

from flask import json
from werkzeug.exceptions import HTTPException
from flask_example import app
 
@app.errorhandler(HTTPException)
def handle_exception(e):
    """Return JSON instead of HTML for HTTP errors."""
    # start with the correct headers and status code from the error
    response = e.get_response()
    # replace the body with JSON
    response.data = json.dumps({
            "code": e.code,
            "name": e.name,
            "description": e.description,
    })
    response.content_type = "application/json"
    return response

Http requests

Add person with 2 addresses:
curl  -H "Content-Type: application/json" -d '{"lastname":"David", "firstname":"boyoDoe", "addresses":[{"street":"8 oxford street","country_code":"UK", 
"zip_code":"99999"},{"street":"12 Circus","country_code":"FR", "zip_code":"75001"}]}' localhost:5001/api/person  
 
Add another person with 2 addresses:
curl  -H "Content-Type: application/json" -d '{"lastname":"Jon", "firstname":"fullbar", "addresses":[{"street":"8 special street","country_code":"US", 
"zip_code":"99999"},{"street":"12 Zoo","country_code":"FR", "zip_code":"75001"}]}' localhost:5001/api/person  
 
Get person and his address by id:<br />curl -X GET localhost:5001/api/person/1
curl -X GET localhost:5001/api/person/1?fetch_address
 
get all persons:
curl -X GET localhost:5001/api/persons
 
get all persons by filtering on the country code of the address and the last name of the person :
curl -X GET "localhost:5001/api/persons?country_code=FR&lastname=Jon" 
 
update person:
curl -X PUT  -H "Content-Type: application/json" -d '{"id": 1, "lastname":"Davido", "firstname":"noename", "addresses":[{"street":"12 Google", 
"country_code":"FR", "zip_code":"75001"}]}' localhost:5001/api/person/1
Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *