Skip to content

Commit

Permalink
Books, Authors, marshmallow schemas, and others (#1)
Browse files Browse the repository at this point in the history
* ci: rename pipeline script

* feat: ✨ implement get and create methods for books and authors

post and get methods implemented for both resources, pydantic removed, mashmallow schemas implemented, request validator decorator created

* fix: adjust unit tests
  • Loading branch information
gabrielborgesdm authored May 17, 2024
1 parent 0668317 commit 5a4b5e9
Show file tree
Hide file tree
Showing 23 changed files with 363 additions and 201 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Pylint
on: [push]
jobs:
Backend-Pipeline:
Backend-Linter:
runs-on: ubuntu-latest
env:
PYTHON_VERSION: "3.10.12"
Expand Down
14 changes: 7 additions & 7 deletions backend/app/controllers/author_controller.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from flask import Blueprint
from flask_pydantic import validate
from flask import Blueprint, g

from backend.app.dtos.author_dto import AuthorCreateDto
from backend.app.decorators.validation_decorator import validate_schema
from backend.app.services.management_service import ManagementService
from backend.app.schemas.author_schema import author_create_schema


blueprint = Blueprint("authors", __name__, url_prefix="/authors")
Expand All @@ -17,8 +17,8 @@ def get_all():


@blueprint.route("/", methods=["POST"])
@validate()
def create(body: AuthorCreateDto):
management_service.create_author(body)
@validate_schema(author_create_schema)
def create():
author = management_service.create_author(g.validated_data)

return body.__dict__, 201
return author, 201
12 changes: 6 additions & 6 deletions backend/app/controllers/book_controller.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from flask import Blueprint
from flask_pydantic import validate
from flask import Blueprint, g

from backend.app.dtos.book_dto import BookCreateDto
from backend.app.decorators.validation_decorator import validate_schema
from backend.app.services.management_service import ManagementService
from backend.app.schemas.book_schema import book_create_schema


blueprint = Blueprint("books", __name__, url_prefix="/books")
Expand All @@ -17,8 +17,8 @@ def get_all():


@blueprint.route("/", methods=["POST"])
@validate()
def create(body: BookCreateDto):
book = management_service.create_book(body)
@validate_schema(book_create_schema)
def create():
book = management_service.create_book(g.validated_data)

return book, 201
23 changes: 23 additions & 0 deletions backend/app/decorators/validation_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from flask import g, jsonify, request
from marshmallow import ValidationError
from werkzeug.exceptions import BadRequest


def validate_schema(schema):
def decorator(f):
def wrapper(*args, **kwargs):
json_data = request.get_json()
if not json_data:
return jsonify({"message": "No input data provided"}), 400

try:
data = schema.load(json_data)
g.validated_data = data
except ValidationError as err:
raise BadRequest(err.messages) from err
return f(*args, **kwargs)

wrapper.__name__ = f.__name__
return wrapper

return decorator
17 changes: 0 additions & 17 deletions backend/app/dtos/author_dto.py

This file was deleted.

34 changes: 0 additions & 34 deletions backend/app/dtos/book_dto.py

This file was deleted.

4 changes: 2 additions & 2 deletions backend/app/models/author_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AuthorModel(db.Model):

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(300), nullable=False)
email = db.Column(db.String(320), nullable=True)
nationality = db.Column(db.String(100), nullable=False)
birthDate = db.Column(db.Date, nullable=False)
email = db.Column(db.String(320), nullable=True)
birthDate = db.Column(db.Date, nullable=True)
books = db.relationship("BookModel", secondary=BookAuthor, back_populates="authors")
32 changes: 32 additions & 0 deletions backend/app/schemas/author_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from marshmallow import Schema, fields, validate
from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field
from marshmallow_sqlalchemy.fields import Nested

from backend.app.models.author_model import AuthorModel


class AuthorCreateSchema(Schema):
name = fields.Str(required=True, validate=validate.Length(min=1, max=300))
email = fields.Email(validate=validate.Length(min=1, max=320))
nationality = fields.Str(required=True, validate=validate.Length(min=1, max=100))
birthDate = fields.Date()


class AuthorSchema(SQLAlchemySchema):
class Meta:
model = AuthorModel
include_relationships = True
load_instance = True

id = auto_field()
name = auto_field()
email = auto_field()
nationality = auto_field()
birthDate = auto_field()
books = Nested("BookSchema", many=True, exclude=["authors"])


author_schema = AuthorSchema()
authors_schema = AuthorSchema(many=True)

author_create_schema = AuthorCreateSchema()
45 changes: 45 additions & 0 deletions backend/app/schemas/book_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from marshmallow import Schema, fields, validate, validates
from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field
from marshmallow_sqlalchemy.fields import Nested
from werkzeug.exceptions import BadRequest

from backend.app.models.book_model import BookModel
from backend.app.schemas.author_schema import AuthorSchema, AuthorCreateSchema


class AuthorFindOrCreateSchema(Schema):
existentAuthorId = fields.Int()
authorCreationPayload = fields.Nested(AuthorCreateSchema)


class BookCreateSchema(Schema):
title = fields.Str(required=True, validate=validate.Length(max=300))
authors = fields.Nested(AuthorFindOrCreateSchema, many=True)
pages = fields.Int(required=True)

@validates("authors")
def items_must_not_be_empty(self, value):
if not value:
raise BadRequest("List of authors cannot be empty")

for item in value:
if not item.get("existentAuthorId") and not item.get("authorCreationPayload"):
raise BadRequest("existentAuthorId or authorCreationPayload is required")


class BookSchema(SQLAlchemySchema):
class Meta:
model = BookModel
include_relationships = True
load_instance = True

id = auto_field()
title = auto_field()
authors = Nested(AuthorSchema, many=True, exclude=["books"])
pages = auto_field()


book_schema = BookSchema()
books_schema = BookSchema(many=True)

book_create_schema = BookCreateSchema()
49 changes: 36 additions & 13 deletions backend/app/services/management_service.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
from typing import Dict, List
from werkzeug.exceptions import BadRequest

from backend.app.models.model import db
from backend.app.models.author_model import AuthorModel
from backend.app.models.book_model import BookModel
from backend.app.dtos.book_dto import BookCreateDto, BookResponseDto
from backend.app.dtos.author_dto import AuthorCreateDto, AuthorResponseDto
from backend.app.models.model import db
from backend.app.schemas.author_schema import AuthorSchema, author_schema, authors_schema
from backend.app.schemas.book_schema import AuthorFindOrCreateSchema, books_schema, book_schema


class ManagementService:
def get_authors(self):
authors = AuthorModel.query.all()

return [AuthorResponseDto(**author.__dict__).__dict__ for author in authors]
return authors_schema.dump(authors)

def get_books(self):
books = BookModel.query.all()

return [BookResponseDto(**book.__dict__).__dict__ for book in books]
return books_schema.dump(books)

def create_author(self, author: AuthorCreateDto):
data = author.__dict__
def create_author(self, author_data: Dict) -> AuthorSchema:
with db.session.begin():
author = AuthorModel(**data)
author = AuthorModel(**author_data)
db.session.add(author)
db.session.commit()
return author_schema.dump(author)

def create_book(self, book: BookCreateDto):
print(book)
def create_book(self, book_dto: Dict):
with db.session.begin():
books = BookModel.query.all()
authors = self._get_authors_for_book_creation(book_dto.get("authors"))
del book_dto["authors"]
book = BookModel(**book_dto)
book.authors = authors
db.session.add(book)

return book_schema.dump(book)

def _get_authors_for_book_creation(self, authors_dto: List[AuthorFindOrCreateSchema]):
authors = [self._find_or_create_author(author) for author in authors_dto]

return authors

def _find_or_create_author(
self, author_dto: Dict[str, AuthorFindOrCreateSchema]
) -> AuthorModel:
existent_id = author_dto.get("existentAuthorId")
if existent_id is None:
return AuthorModel(**author_dto.get("authorCreationPayload"))

author = AuthorModel.query.get(existent_id)
if author is None:
raise BadRequest(f"author of id {existent_id} was not found")

return books
return author
3 changes: 3 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from flask import Flask
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from backend.app.handlers.http_error_handler import handle_exception
from backend.config import Config

from backend.app.models.model import db

from backend.app.controllers.book_controller import blueprint as books_bp
Expand All @@ -19,3 +21,4 @@
app.register_error_handler(400, handle_exception)

migrate = Migrate(app, db)
ma = Marshmallow(app)
51 changes: 51 additions & 0 deletions backend/migrations/versions/17eb199d7416_initial_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Initial commit
Revision ID: 17eb199d7416
Revises:
Create Date: 2024-05-16 16:58:03.648098
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '17eb199d7416'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('Author',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=300), nullable=False),
sa.Column('email', sa.String(length=320), nullable=True),
sa.Column('nationality', sa.String(length=100), nullable=False),
sa.Column('birthDate', sa.Date(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('Book',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=300), nullable=False),
sa.Column('pages', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('BookAuthor',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bookId', sa.Integer(), nullable=True),
sa.Column('authorId', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['authorId'], ['Author.id'], ),
sa.ForeignKeyConstraint(['bookId'], ['Book.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('BookAuthor')
op.drop_table('Book')
op.drop_table('Author')
# ### end Alembic commands ###
Loading

0 comments on commit 5a4b5e9

Please sign in to comment.