-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from gablooge/6-feat-implement-database-model
feat: model bills
- Loading branch information
Showing
17 changed files
with
403 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from sqlalchemy import CheckConstraint, Column, Float, ForeignKey, Index, String | ||
from sqlalchemy.orm import Mapped, mapped_column, relationship | ||
from sqlalchemy.sql import func | ||
|
||
from uselevers.core.models import Base, MixinCreatedUpdated, id_gen, id_len | ||
|
||
|
||
class Bill(Base, MixinCreatedUpdated): | ||
__tablename__ = "bills" # type: ignore[assignment] | ||
id: Mapped[str] = mapped_column( | ||
String(id_len), | ||
default=id_gen("B"), | ||
primary_key=True, | ||
) | ||
total = mapped_column(Float, nullable=False) | ||
|
||
sub_bills = relationship( | ||
"SubBill", | ||
back_populates="bill", | ||
cascade="all, delete-orphan", | ||
lazy="dynamic", | ||
passive_deletes=True, | ||
foreign_keys="SubBill.bill_id", | ||
) | ||
|
||
# Add a CheckConstraint to enforce that total must be positive | ||
__table_args__ = (CheckConstraint("total >= 0", name="check_positive_total"),) | ||
|
||
|
||
class SubBill(Base, MixinCreatedUpdated): | ||
__tablename__ = "sub_bills" # type: ignore[assignment] | ||
id: Mapped[str] = mapped_column( | ||
String(id_len), | ||
default=id_gen("SB"), | ||
primary_key=True, | ||
) | ||
bill_id = Column(String, ForeignKey("bills.id", ondelete="CASCADE"), nullable=False) | ||
amount = mapped_column(Float, nullable=False) | ||
reference = Column(String, nullable=True) | ||
|
||
bill = relationship("Bill", back_populates="sub_bills", foreign_keys=[bill_id]) | ||
|
||
__table_args__ = ( | ||
Index("unique_reference_case_insensitive", func.lower(reference), unique=True), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from typing import Annotated | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from uselevers.core.models import id_len | ||
from uselevers.core.schemas import MixinCreatedUpdated | ||
|
||
|
||
class BillSpec(BaseModel): | ||
total: Annotated[float, Field(description="Total")] | ||
|
||
|
||
class Bill(MixinCreatedUpdated, BillSpec, BaseModel): | ||
id: Annotated[ | ||
str, | ||
Field( | ||
description="Bill ID", | ||
max_length=id_len, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from typing import Annotated | ||
|
||
import pytest | ||
from fastapi import Depends | ||
from fastapi.encoders import jsonable_encoder | ||
from sqlalchemy.exc import IntegrityError | ||
from sqlalchemy.orm import Session | ||
|
||
from uselevers.bills.models import Bill | ||
from uselevers.bills.schemas import BillSpec | ||
from uselevers.core import deps | ||
from uselevers.tests.conftest import SessionTest, alembic_engine, db # noqa: F401,F811 | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"total", | ||
[ | ||
(0), | ||
(0.5), | ||
(1.00), | ||
(1.05), | ||
], | ||
) | ||
def test_crud_bills( | ||
db: Annotated[Session, Depends(deps.get_db)], # noqa: F811 | ||
total: float, | ||
) -> None: | ||
bill_create = BillSpec( | ||
total=total, | ||
) | ||
with db.begin(): | ||
# test create | ||
assert db.query(Bill).count() == 0 | ||
instance = Bill(**jsonable_encoder(bill_create)) | ||
db.add(instance) | ||
# write the object to the database | ||
db.flush() | ||
assert db.query(Bill).count() == 1 | ||
|
||
# test read | ||
created_bill = db.query(Bill).filter(Bill.id == instance.id).first() | ||
assert created_bill is not None | ||
assert created_bill.total == total | ||
assert created_bill.id[0] == "B" | ||
|
||
# test update with invalid total | ||
with pytest.raises(IntegrityError): | ||
created_bill.total = -1 | ||
db.flush() | ||
|
||
# test update and delete | ||
with db.begin(): | ||
created_bill = db.query(Bill).first() | ||
assert created_bill is not None | ||
# test update with valid total | ||
new_total: float = 1.10 | ||
created_bill.total = new_total | ||
db.flush() | ||
updated_bill = db.query(Bill).filter(Bill.id == created_bill.id).first() | ||
assert updated_bill is not None | ||
assert updated_bill.total == new_total | ||
assert updated_bill.id[0] == "B" | ||
|
||
# test delete | ||
db.delete(updated_bill) | ||
db.flush() | ||
assert db.query(Bill).count() == 0 | ||
|
||
|
||
def test_create_bill_with_invalid_total( | ||
db: Annotated[Session, Depends(deps.get_db)], # noqa: F811 | ||
) -> None: | ||
bill_create = BillSpec( | ||
total=-1, | ||
) | ||
with db.begin(): | ||
# test create | ||
assert db.query(Bill).count() == 0 | ||
with pytest.raises(IntegrityError): | ||
instance = Bill(**jsonable_encoder(bill_create)) | ||
db.add(instance) | ||
db.flush() | ||
|
||
with db.begin(): | ||
assert db.query(Bill).count() == 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# For detecting the alembic `autogenerate` revisions from core.models.Base.metadata | ||
|
||
from uselevers.auth import models as auth_models # noqa: F401 | ||
from uselevers.bills import models as bills_models # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from datetime import datetime | ||
from typing import Annotated | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
|
||
class MixinCreatedUpdated(BaseModel): | ||
created: Annotated[datetime, Field(description="Created at")] | ||
updated: Annotated[datetime, Field(description="Updated at")] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,29 @@ | ||
Generic single-database configuration. | ||
|
||
|
||
Some alembic example commands | ||
|
||
```sh | ||
# generate migration | ||
docker compose -f docker-compose.dev.yml run --rm app alembic revision --autogenerate -m "Describe this migration" | ||
# Look the generated migrations versions, check if there's new ENUM, make sure add drop type e.g op.execute("DROP TYPE enumname") in downgrade() | ||
|
||
# alternatively to generate the alembic migration locally with poetry | ||
poetry run alembic revision --autogenerate -m "Describe this migration" | ||
|
||
# show histories | ||
docker compose -f docker-compose.dev.yml run --rm app alembic history | ||
# or | ||
poetry run alembic history | ||
|
||
# Running migration | ||
docker compose -f docker-compose.dev.yml run --rm app alembic upgrade head | ||
# or | ||
poetry run alembic upgrade head | ||
|
||
# Rollout | ||
poetry run alembic upgrade +1 | ||
|
||
# Rollback | ||
poetry run alembic downgrade -1 | ||
``` |
77 changes: 77 additions & 0 deletions
77
uselevers/migrations/versions/0b8a85b2cfc4_init_bill_tables.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
""" | ||
init_bill_tables | ||
Revision ID: 0b8a85b2cfc4 | ||
Revises: 109d2489d83f | ||
Create Date: 2024-04-08 05:36:44.651645 | ||
""" | ||
|
||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "0b8a85b2cfc4" | ||
down_revision = "109d2489d83f" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.create_table( | ||
"bills", | ||
sa.Column("id", sa.String(length=32), nullable=False), | ||
sa.Column("total", sa.Float(), nullable=False), | ||
sa.Column( | ||
"updated", | ||
sa.DateTime(), | ||
server_default=sa.text("NOW()::timestamp"), | ||
nullable=False, | ||
), | ||
sa.Column( | ||
"created", | ||
sa.DateTime(), | ||
server_default=sa.text("NOW()::timestamp"), | ||
nullable=False, | ||
), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
op.create_table( | ||
"sub_bills", | ||
sa.Column("id", sa.String(length=32), nullable=False), | ||
sa.Column("bill_id", sa.String(), nullable=False), | ||
sa.Column("amount", sa.Float(), nullable=False), | ||
sa.Column("reference", sa.String(), nullable=True), | ||
sa.Column( | ||
"updated", | ||
sa.DateTime(), | ||
server_default=sa.text("NOW()::timestamp"), | ||
nullable=False, | ||
), | ||
sa.Column( | ||
"created", | ||
sa.DateTime(), | ||
server_default=sa.text("NOW()::timestamp"), | ||
nullable=False, | ||
), | ||
sa.ForeignKeyConstraint(["bill_id"], ["bills.id"], ondelete="CASCADE"), | ||
sa.PrimaryKeyConstraint("id"), | ||
) | ||
op.create_index( | ||
"unique_reference_case_insensitive", | ||
"sub_bills", | ||
[sa.text("lower(reference)")], | ||
unique=True, | ||
) | ||
op.create_check_constraint("check_positive_total", "bills", "total >= 0") | ||
# ### end Alembic commands ### | ||
|
||
|
||
def downgrade() -> None: | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_constraint("check_positive_total", "bills") | ||
op.drop_index("unique_reference_case_insensitive", table_name="sub_bills") | ||
op.drop_table("sub_bills") | ||
op.drop_table("bills") | ||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.