Skip to content

Commit

Permalink
Add books model and post endpoint to create new books
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurcostaa committed Sep 1, 2024
1 parent 6458f83 commit 233a2e8
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 8 deletions.
13 changes: 9 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ services:
image: postgres
volumes:
- pgdata:/var/lib/postgresql/data
env_file:
- .env
environment:
POSTGRES_USER: app_user
POSTGRES_DB: app_db
POSTGRES_PASSWORD: app_password
ports:
- "5432:5432"

madr_app:
image: madr
entrypoint: ./entrypoint.sh
build: .
env_file:
- .env
environment:
DATABASE_URL: postgresql+psycopg://app_user:app_password@madr_database:5432/app_db
SECRET_KEY: "secure secret key"
ALGORITHM: "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: 60
ports:
- "8000:8000"
depends_on:
Expand Down
3 changes: 2 additions & 1 deletion madr/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

from fastapi import FastAPI

from madr.routers import auth, novelists, users
from madr.routers import auth, books, novelists, users
from madr.schemas import Message

app = FastAPI()

app.include_router(auth.router)
app.include_router(books.router)
app.include_router(users.router)
app.include_router(novelists.router)

Expand Down
28 changes: 26 additions & 2 deletions madr/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped, mapped_column, registry
from sqlalchemy import ForeignKey, func
from sqlalchemy.orm import Mapped, mapped_column, registry, relationship

table_registry = registry()

Expand All @@ -28,6 +28,30 @@ class Novelist:

id: Mapped[int] = mapped_column(init=False, primary_key=True)
name: Mapped[str] = mapped_column(unique=True, nullable=False)
books: Mapped[list['Book']] = relationship(
init=False, back_populates='novelist', cascade='all, delete-orphan'
)
created_at: Mapped[datetime] = mapped_column(
init=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
init=False, server_default=func.now(), onupdate=func.now()
)


@table_registry.mapped_as_dataclass
class Book:
__tablename__ = 'books'

id: Mapped[int] = mapped_column(init=False, primary_key=True)
year: Mapped[int] = mapped_column(nullable=False)
title: Mapped[str] = mapped_column(unique=True, nullable=False)
novelist_id: Mapped[int] = mapped_column(
ForeignKey('novelists.id'), init=False
)
novelist: Mapped[Novelist] = relationship(
init=False, back_populates='books'
)
created_at: Mapped[datetime] = mapped_column(
init=False, server_default=func.now()
)
Expand Down
47 changes: 47 additions & 0 deletions madr/routers/books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from http import HTTPStatus
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from madr.database import get_session
from madr.models import Book, Novelist, User
from madr.schemas import BookPublic, BookSchema
from madr.security import get_current_user
from madr.utils import sanitize

router = APIRouter(prefix='/books', tags=['books'])

T_CurrentUser = Annotated[User, Depends(get_current_user)]
T_Session = Annotated[Session, Depends(get_session)]


@router.post('/', status_code=HTTPStatus.CREATED, response_model=BookPublic)
def create_book(book: BookSchema, session: T_Session, user: T_CurrentUser):
novelist = session.scalar(
select(Novelist).where(Novelist.id == book.novelist_id)
)

if not novelist:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail='Novelist ID not found',
)

new_book = Book(year=book.year, title=sanitize(book.title))
new_book.novelist = novelist

try:
session.add(new_book)
session.commit()
session.refresh(new_book)
except IntegrityError:
session.rollback()
raise HTTPException(
status_code=HTTPStatus.CONFLICT,
detail='Book already exists in MADR',
)

return new_book
10 changes: 10 additions & 0 deletions madr/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ class NovelistPublic(NovelistSchema):

class NovelistList(BaseModel):
novelists: list[NovelistPublic]


class BookSchema(BaseModel):
year: int
title: str = Field(min_length=3, max_length=256)
novelist_id: int


class BookPublic(BookSchema):
id: int
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""create books table and relationship with novelists
Revision ID: cba12017681b
Revises: 5bd57142ab29
Create Date: 2024-09-01 13:30:20.688056
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'cba12017681b'
down_revision: Union[str, None] = '5bd57142ab29'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('books',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('year', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('novelist_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['novelist_id'], ['novelists.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('title')
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('books')
# ### end Alembic commands ###
13 changes: 12 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from madr.app import app
from madr.database import get_session
from madr.models import Novelist, User, table_registry
from madr.models import Book, Novelist, User, table_registry
from madr.security import get_password_hash


Expand Down Expand Up @@ -115,3 +115,14 @@ def other_novelist(session):
session.refresh(novelist)

return novelist


@pytest.fixture
def book(session, novelist):
new_book = Book(year=2024, title='book1')
new_book.novelist = novelist

session.add(new_book)
session.commit()

return new_book
66 changes: 66 additions & 0 deletions tests/test_books.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from http import HTTPStatus


def test_create_book(client, novelist, token):
response = client.post(
'/books/',
headers={'Authorization': f'Bearer {token}'},
json={
'year': 2024,
'title': 'New Book',
'novelist_id': novelist.id,
},
)

assert response.status_code == HTTPStatus.CREATED
assert response.json() == {
'id': 1,
'year': 2024,
'novelist_id': novelist.id,
'title': 'new book',
}


def test_create_book_with_unexistent_novelist(client, token):
response = client.post(
'/books/',
headers={'Authorization': f'Bearer {token}'},
json={
'year': 2024,
'title': 'the best book of all time ever',
'novelist_id': 1,
},
)

assert response.status_code == HTTPStatus.NOT_FOUND
assert response.json() == {'detail': 'Novelist ID not found'}


def test_create_book_already_existent(client, token, novelist, book):
response = client.post(
'/books/',
headers={'Authorization': f'Bearer {token}'},
json={
'year': 2024,
'title': book.title,
'novelist_id': novelist.id,
},
)

assert response.status_code == HTTPStatus.CONFLICT
assert response.json() == {'detail': 'Book already exists in MADR'}


def test_create_book_with_unexistent_novelist_id(client, token):
response = client.post(
'/books/',
headers={'Authorization': f'Bearer {token}'},
json={
'year': 2024,
'title': 'Book',
'novelist_id': 1,
},
)

assert response.status_code == HTTPStatus.NOT_FOUND
assert response.json() == {'detail': 'Novelist ID not found'}

0 comments on commit 233a2e8

Please sign in to comment.