From 64e00e9c0072854a4f7e76162b351d497f8b158c Mon Sep 17 00:00:00 2001 From: Tomas Gruner <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:46:52 -0300 Subject: [PATCH 01/24] fix: restrict category endpoints (+others) --- src/main.py | 159 ++++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/src/main.py b/src/main.py index 1899187..bf8e306 100755 --- a/src/main.py +++ b/src/main.py @@ -81,73 +81,17 @@ def login(user: schemas.UserLogin, db: DbDependency) -> schemas.UserCredentials: ################################################ -# CATEGORIES +# GROUPS ################################################ -@app.post("/category", status_code=HTTPStatus.CREATED) -def create_category(category: schemas.CategoryCreate, db: DbDependency): - group = crud.get_group_by_id(db, category.group_id) - if group is None: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Grupo inexistente" - ) - return crud.create_category(db, category) - - -@app.get("/category/{category_id}") -def update_category(db: DbDependency, category_id: int): - category = crud.get_category_by_id(db, category_id) - if category is None: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" - ) - - return category - - -@app.put("/category/{category_id}") -def update_category( - category_update: schemas.CategoryUpdate, db: DbDependency, category_id: int -): - category = crud.get_category_by_id(db, category_id) - if category is None: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" - ) - - return crud.update_category(db, category, category_update) - - -@app.delete("/category/{category_id}") -def delete_category(db: DbDependency, category_id: int): - category_to_delete = crud.get_category_by_id(db, category_id) - if category_to_delete is None: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" - ) - - return crud.delete_category(db, category_to_delete) - - -@app.get("/group/{group_id}/category") -def list_group_categories(db: DbDependency, group_id: int): - group = crud.get_group_by_id(db, group_id) - - if group is None: +def check_group_is_unarchived(group: models.Group): + if group.is_archived: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Grupo inexistente" + status_code=HTTPStatus.NOT_ACCEPTABLE, + detail="El grupo esta archivado, no se pueden realizar modificaciones", ) - categories = crud.get_categories_by_group_id(db, group_id) - - return categories - - -################################################ -# GROUPS -################################################ - def user_id_in_group(user_id: int, group: models.Group) -> bool: return any(member.id == user_id for member in group.members) @@ -190,6 +134,7 @@ def update_group( group_to_update = crud.get_group_by_id(db, put_group.id) check_group_exists_and_user_is_owner(user.id, group_to_update) + check_group_is_unarchived(group_to_update) return crud.update_group(db, group_to_update, put_group) @@ -218,6 +163,7 @@ def add_user_to_group( group = crud.get_group_by_id(db, group_id) check_group_exists_and_user_is_owner(user.id, group) + check_group_is_unarchived(group) crud.add_user_to_group(db, group, req.user_id) @@ -233,9 +179,6 @@ def list_group_members(db: DbDependency, user: UserDependency, group_id: int): return group.members -# TODO: CUANDO TERMINEN IMPLEMENTAR EL INVITE Y JOIN, NO DEJEN INVITAR NI ACEPTAR NI NADA SI EL GRUPO ESTA ARCHIVADO. - - @app.put("/group/{group_id}/archive", status_code=HTTPStatus.OK) def archive_group(db: DbDependency, user: UserDependency, group_id: int): group = crud.get_group_by_id(db, group_id) @@ -256,6 +199,74 @@ def unarchive_group(db: DbDependency, user: UserDependency, group_id: int): return {"detail": f"Grupo {archived_group.name} desarchivado correctamente"} +################################################ +# CATEGORIES +################################################ + + +@app.post("/category", status_code=HTTPStatus.CREATED) +def create_category( + category: schemas.CategoryCreate, + db: DbDependency, + user: UserDependency, +): + group = crud.get_group_by_id(db, category.group_id) + check_group_exists_and_user_is_owner(user.id, group) + check_group_is_unarchived(group) + return crud.create_category(db, category) + + +@app.get("/category/{category_id}") +def get_category(db: DbDependency, user: UserDependency, category_id: int): + category = crud.get_category_by_id(db, category_id) + if category is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" + ) + group = crud.get_group_by_id(db, category.group_id) + check_group_exists_and_user_is_member(user.id, group) + return category + + +@app.put("/category/{category_id}") +def update_category( + category_update: schemas.CategoryUpdate, + db: DbDependency, + user: UserDependency, + category_id: int, +): + category = crud.get_category_by_id(db, category_id) + if category is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" + ) + group = crud.get_group_by_id(db, category.group_id) + check_group_exists_and_user_is_owner(user.id, group) + check_group_is_unarchived(group) + return crud.update_category(db, category, category_update) + + +@app.delete("/category/{category_id}") +def delete_category(db: DbDependency, user: UserDependency, category_id: int): + category = crud.get_category_by_id(db, category_id) + if category is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" + ) + group = crud.get_group_by_id(db, category.group_id) + check_group_exists_and_user_is_owner(user.id, group) + check_group_is_unarchived(group) + return crud.delete_category(db, category) + + +@app.get("/group/{group_id}/category") +def list_group_categories(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + check_group_exists_and_user_is_member(user.id, group) + categories = crud.get_categories_by_group_id(db, group_id) + return categories + + ################################################ # SPENDINGS ################################################ @@ -268,12 +279,7 @@ def create_spending( group = crud.get_group_by_id(db, spending.group_id) check_group_exists_and_user_is_member(user.id, group) - - if group.is_archived: - raise HTTPException( - status_code=HTTPStatus.NOT_ACCEPTABLE, - detail="El grupo esta archivado, no se pueden seguir agregando gastos.", - ) + check_group_is_unarchived(group) category = crud.get_category_by_id(db, spending.category_id) if category is None or category.group_id != spending.group_id: @@ -314,12 +320,8 @@ def create_budget( group = crud.get_group_by_id(db, spending.group_id) check_group_exists_and_user_is_owner(user.id, group) + check_group_is_unarchived(group) - if group.is_archived: - raise HTTPException( - status_code=HTTPStatus.NOT_ACCEPTABLE, - detail="El grupo esta archivado, no se pueden seguir agregando presupuestos.", - ) return crud.create_budget(db, spending) @@ -346,12 +348,8 @@ def put_budget( group = crud.get_group_by_id(db, db_budget.group_id) check_group_exists_and_user_is_member(user.id, group) + check_group_is_unarchived(group) - if group.is_archived: - raise HTTPException( - status_code=HTTPStatus.NOT_ACCEPTABLE, - detail="El grupo esta archivado, no se pueden seguir agregando presupuestos.", - ) return crud.put_budget(db, db_budget, put_budget) @@ -397,6 +395,7 @@ def send_invite( target_group = crud.get_group_by_id(db, invite.group_id) check_group_exists_and_user_is_owner(user.id, target_group) + check_group_is_unarchived(target_group) if user_id_in_group(receiver.id, target_group): raise HTTPException( From 40f9fd0eb35f70bd08bfe3e819cd3126ad854159 Mon Sep 17 00:00:00 2001 From: Tomas Gruner <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 1 Jun 2024 22:06:35 -0300 Subject: [PATCH 02/24] feat: add transactions and balances --- Makefile | 2 +- src/crud.py | 70 ++++++++++++++++++++++++-- src/main.py | 11 ++++- src/models.py | 24 +++++++++ src/test_main.py | 126 +++++++++++++++++++++++++++++++++++++---------- 5 files changed, 201 insertions(+), 32 deletions(-) diff --git a/Makefile b/Makefile index e19ba5f..6bb2b48 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ lint: test: rm test.db 2> /dev/null || true - DB_NAME="./test.db" poetry run pytest -svv . + DB_NAME="./test.db" poetry run pytest -svvx --ff . CONTAINER_NAME="postgres_test_db" diff --git a/src/crud.py b/src/crud.py index fe4a583..4ea02b9 100755 --- a/src/crud.py +++ b/src/crud.py @@ -1,3 +1,4 @@ +from typing import List from sqlalchemy import delete, select from sqlalchemy.orm import Session from uuid import UUID @@ -87,8 +88,10 @@ def create_group(db: Session, group: schemas.GroupCreate, user_id: int): # Add the owner to the group members db_user = get_user_by_id(db, user_id) - db_user.groups.add(db_group) + db.commit() + db.refresh(db_group) + _add_user_to_group(db, db_user, db_group) db.commit() db.refresh(db_group) return db_group @@ -125,14 +128,19 @@ def update_group_status(db: Session, group: models.Group, status: bool): return group -def add_user_to_group(db: Session, group: models.Group, user_id: int): +def add_user_to_group(db: Session, user_id: int, group: models.Group): user = get_user_by_id(db, user_id) - group.members.add(user) + _add_user_to_group(db, user, group) db.commit() db.refresh(group) return group +def _add_user_to_group(db: Session, user: models.User, group: models.Group): + group.members.add(user) + create_user_balance(db, user.id, group.id) + + ################################################ # SPENDINGS ################################################ @@ -143,6 +151,8 @@ def create_spending(db: Session, spending: schemas.SpendingCreate, user_id: int) db.add(db_spending) db.commit() db.refresh(db_spending) + create_transactions_from_spending(db, db_spending) + db.refresh(db_spending) return db_spending @@ -233,3 +243,57 @@ def update_invite_status( db.commit() db.refresh(db_invite) return db_invite + + +################################################ +# TRANSACTIONS +################################################ + + +def create_transactions_from_spending(db: Session, spending: models.Spending): + group = get_group_by_id(db, spending.group_id) + balances = sorted( + get_balances_by_group_id(db, spending.group_id), key=lambda x: x.user_id + ) + members = sorted(group.members, key=lambda x: x.id) + # TODO: implement division strategy + # TODO: this truncates results when the amount is not divisible by the number of members + amount_per_member = spending.amount // len(members) + txs = [] + for user, balance in zip(members, balances): + amount = -amount_per_member + + if spending.owner_id == user.id: + amount += spending.amount + + tx = models.Transaction( + from_user_id=spending.owner_id, + to_user_id=user.id, + amount=amount, + spending_id=spending.id, + ) + txs.append(tx) + db.add(tx) + balance.current_balance += amount + + db.commit() + + +################################################ +# BALANCES +################################################ + + +# NOTE: doesn't commit transaction +def create_user_balance(db: Session, user_id: int, group_id: int): + balance = models.Balance(user_id=user_id, group_id=group_id, current_balance=0) + db.add(balance) + + +def get_balances_by_group_id(db: Session, group_id: int) -> List[models.Balance]: + return ( + db.query(models.Balance) + .filter(models.Balance.group_id == group_id) + .limit(100) + .all() + ) diff --git a/src/main.py b/src/main.py index bf8e306..c7c0682 100755 --- a/src/main.py +++ b/src/main.py @@ -165,7 +165,7 @@ def add_user_to_group( check_group_exists_and_user_is_owner(user.id, group) check_group_is_unarchived(group) - crud.add_user_to_group(db, group, req.user_id) + crud.add_user_to_group(db, req.user_id, group) return group.members @@ -199,6 +199,13 @@ def unarchive_group(db: DbDependency, user: UserDependency, group_id: int): return {"detail": f"Grupo {archived_group.name} desarchivado correctamente"} +@app.get("/group/{group_id}/balance") +def list_group_balances(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + check_group_exists_and_user_is_member(user.id, group) + return crud.get_balances_by_group_id(db, group_id) + + ################################################ # CATEGORIES ################################################ @@ -453,5 +460,5 @@ def accept_invite(db: DbDependency, user: UserDependency, invite_token: str): detail=f"El usuario ya es miembro del grupo {target_group.name}", ) - crud.add_user_to_group(db, target_group, user.id) + crud.add_user_to_group(db, user.id, target_group) return crud.update_invite_status(db, target_invite, schemas.InviteStatus.ACCEPTED) diff --git a/src/models.py b/src/models.py index c0f61e0..d03d733 100755 --- a/src/models.py +++ b/src/models.py @@ -98,3 +98,27 @@ class Invite(Base): token = Column(UUID(as_uuid=True), unique=True, default=uuid4) status = Column(Enum(InviteStatus)) creation_date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + + +class Transaction(Base): + __tablename__ = "transactions" + + id = Column(Integer, primary_key=True) + spending_id = Column(ForeignKey("spendings.id")) + from_user_id = Column(ForeignKey("users.id")) + to_user_id = Column(ForeignKey("users.id")) + date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + amount = Column(Integer) + + __table_args__ = (UniqueConstraint("spending_id", "to_user_id"),) + + +class Balance(Base): + __tablename__ = "balances" + + id = Column(Integer, primary_key=True) + user_id = Column(ForeignKey("users.id")) + group_id = Column(ForeignKey("groups.id")) + current_balance = Column(Integer) + + __table_args__ = (UniqueConstraint("user_id", "group_id"),) diff --git a/src/test_main.py b/src/test_main.py index 6059914..0d85ce1 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -40,24 +40,64 @@ def client(): Base.metadata.drop_all(bind=engine) -@pytest.fixture() -def some_credentials(client: TestClient) -> schemas.UserCredentials: +def make_user_credentials(client: TestClient, email: str): response = client.post( url="/user/register", - json={"email": "example@example.com", "password": "my_ultra_secret_password"}, + json={"email": email, "password": "my_ultra_secret_password"}, ) assert response.status_code == HTTPStatus.CREATED return schemas.UserCredentials(**response.json()) +@pytest.fixture() +def some_credentials(client: TestClient) -> schemas.UserCredentials: + return make_user_credentials(client, "example@example.com") + + @pytest.fixture() def some_other_credentials(client: TestClient) -> schemas.UserCredentials: + return make_user_credentials(client, "example2@example.com") + + +@pytest.fixture() +def some_group(client: TestClient, some_credentials: schemas.UserCredentials): response = client.post( - url="/user/register", - json={"email": "example2@example.com", "password": "my_ultra_secret_password"}, + url="/group", + json={"name": "grupo 1", "description": "really long description 1234"}, + headers={"x-user": some_credentials.jwt}, ) + assert response.status_code == HTTPStatus.CREATED - return schemas.UserCredentials(**response.json()) + response_body = response.json() + assert "id" in response_body + assert response_body["owner_id"] == some_credentials.id + return schemas.Group(**response_body) + + +@pytest.fixture() +def some_group_members( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_group: schemas.Group, +): + # NOTE: owner credentials are first item in list + # NOTE: group can be fetched with some_group + number_of_users = 4 + users = [some_credentials] + for i in range(number_of_users): + # Create new user + credentials = make_user_credentials(client, f"user{i}@example.com") + # Add new user to group + response = client.post( + url=f"/group/{some_group.id}/member", + headers={"x-user": some_credentials.jwt}, + json={"user_id": credentials.id}, + ) + assert response.status_code == HTTPStatus.CREATED + + users.append(credentials) + + return users ################################################ @@ -158,21 +198,6 @@ def test_login_with_wrong_email(client: TestClient): ################################################ -@pytest.fixture() -def some_group(client: TestClient, some_credentials: schemas.UserCredentials): - response = client.post( - url="/group", - json={"name": "grupo 1", "description": "really long description 1234"}, - headers={"x-user": some_credentials.jwt}, - ) - - assert response.status_code == HTTPStatus.CREATED - response_body = response.json() - assert "id" in response_body - assert response_body["owner_id"] == some_credentials.id - return schemas.Group(**response_body) - - def test_create_group(client: TestClient, some_group: schemas.Group): # NOTE: test is inside fixture pass @@ -716,7 +741,6 @@ def test_send_group_invite_to_non_registered_user( some_credentials: schemas.UserCredentials, some_group: schemas.Group, ): - response = client.post( url="/invite", json={"receiver_email": "pepe@gmail.com", "group_id": some_group.id}, @@ -754,7 +778,6 @@ def test_send_group_invite_from_non_group_member( some_credentials: schemas.UserCredentials, some_other_credentials: schemas.UserCredentials, ): - # Other Group response = client.post( url="/group", @@ -810,7 +833,6 @@ def test_try_join_group_as_wrong_user( some_credentials: schemas.UserCredentials, some_invite: schemas.Invite, ): - response = client.post( url=f"/invite/join/{some_invite.token.hex}", headers={"x-user": some_credentials.jwt}, @@ -825,7 +847,6 @@ def test_try_join_archived_group( some_group: schemas.Group, some_invite: schemas.Invite, ): - response = client.put( url=f"/group/{some_group.id}/archive", headers={"x-user": some_credentials.jwt} ) @@ -843,7 +864,6 @@ def test_try_join_already_member( some_other_credentials: schemas.UserCredentials, some_invite: schemas.Invite, ): - response = client.post( url=f"/invite/join/{some_invite.token.hex}", headers={"x-user": some_other_credentials.jwt}, @@ -855,3 +875,57 @@ def test_try_join_already_member( headers={"x-user": some_other_credentials.jwt}, ) assert response.status_code == HTTPStatus.BAD_REQUEST + + +################################################ +# BALANCES +################################################ + + +def test_balance_single_group_member( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_spending: schemas.Spending, +): + response = client.get( + url=f"/group/{some_spending.group_id}/balance", + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.OK + + balance_list = response.json() + assert len(balance_list) == 1 + + body = balance_list[0] + assert body["user_id"] == some_credentials.id + assert body["group_id"] == some_spending.group_id + assert body["current_balance"] == 0 + + +# NOTE: parameters need to be in this order +def test_balance_multiple_members( + client: TestClient, + some_group_members: list[schemas.UserCredentials], + some_spending: schemas.Spending, +): + response = client.get( + url=f"/group/{some_spending.group_id}/balance", + headers={"x-user": some_group_members[0].jwt}, + ) + assert response.status_code == HTTPStatus.OK + + balance_list = response.json() + assert len(balance_list) == len(some_group_members) + + charge_per_member = some_spending.amount // len(some_group_members) + assert sum(b["current_balance"] for b in balance_list) == 0 + + balance_list.sort(key=lambda x: x["user_id"]) + + for balance, user in zip(balance_list, some_group_members): + assert balance["user_id"] == user.id + assert balance["group_id"] == some_spending.group_id + expected_balance = -charge_per_member + ( + some_spending.amount if user.id == some_spending.owner_id else 0 + ) + assert balance["current_balance"] == expected_balance From f96612e7bd52ac13a00713a4dce3748344e713e4 Mon Sep 17 00:00:00 2001 From: Tomas Gruner <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 1 Jun 2024 22:18:34 -0300 Subject: [PATCH 03/24] refactor: merge balances and users_to_group table --- src/crud.py | 15 ++------------- src/models.py | 14 +++----------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/crud.py b/src/crud.py index 4ea02b9..2a08c73 100755 --- a/src/crud.py +++ b/src/crud.py @@ -91,7 +91,7 @@ def create_group(db: Session, group: schemas.GroupCreate, user_id: int): db.commit() db.refresh(db_group) - _add_user_to_group(db, db_user, db_group) + db_group.members.add(db_user) db.commit() db.refresh(db_group) return db_group @@ -130,17 +130,12 @@ def update_group_status(db: Session, group: models.Group, status: bool): def add_user_to_group(db: Session, user_id: int, group: models.Group): user = get_user_by_id(db, user_id) - _add_user_to_group(db, user, group) + group.members.add(user) db.commit() db.refresh(group) return group -def _add_user_to_group(db: Session, user: models.User, group: models.Group): - group.members.add(user) - create_user_balance(db, user.id, group.id) - - ################################################ # SPENDINGS ################################################ @@ -284,12 +279,6 @@ def create_transactions_from_spending(db: Session, spending: models.Spending): ################################################ -# NOTE: doesn't commit transaction -def create_user_balance(db: Session, user_id: int, group_id: int): - balance = models.Balance(user_id=user_id, group_id=group_id, current_balance=0) - db.add(balance) - - def get_balances_by_group_id(db: Session, group_id: int) -> List[models.Balance]: return ( db.query(models.Balance) diff --git a/src/models.py b/src/models.py index d03d733..35d0c4e 100755 --- a/src/models.py +++ b/src/models.py @@ -19,14 +19,6 @@ from src.database import Base -user_to_group_table = Table( - "user_to_group_table", - Base.metadata, - Column("user_id", ForeignKey("users.id"), primary_key=True), - Column("group_id", ForeignKey("groups.id"), primary_key=True), -) - - class User(Base): __tablename__ = "users" @@ -34,7 +26,7 @@ class User(Base): email = Column(String, unique=True, index=True) hashed_password = Column(String) groups: Mapped[Set["Group"]] = relationship( - secondary=user_to_group_table, back_populates="members" + secondary="balances", back_populates="members" ) @@ -47,7 +39,7 @@ class Group(Base): description = Column(String) is_archived = Column(Boolean) members: Mapped[Set[User]] = relationship( - secondary=user_to_group_table, back_populates="groups" + secondary="balances", back_populates="groups" ) @@ -119,6 +111,6 @@ class Balance(Base): id = Column(Integer, primary_key=True) user_id = Column(ForeignKey("users.id")) group_id = Column(ForeignKey("groups.id")) - current_balance = Column(Integer) + current_balance = Column(Integer, default=0) __table_args__ = (UniqueConstraint("user_id", "group_id"),) From 58b0b3da8c03430199919c9527a5b1aad947d187 Mon Sep 17 00:00:00 2001 From: Tomas Gruner <47506558+MegaRedHand@users.noreply.github.com> Date: Sat, 1 Jun 2024 22:22:30 -0300 Subject: [PATCH 04/24] chore: run linter and fix errors --- .flake8 | 2 ++ src/crud.py | 4 ++-- src/main.py | 3 +-- src/models.py | 1 - src/test_main.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2bcd70e --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 88 diff --git a/src/crud.py b/src/crud.py index 2a08c73..e2c1473 100755 --- a/src/crud.py +++ b/src/crud.py @@ -1,5 +1,5 @@ from typing import List -from sqlalchemy import delete, select +from sqlalchemy import select from sqlalchemy.orm import Session from uuid import UUID @@ -252,7 +252,7 @@ def create_transactions_from_spending(db: Session, spending: models.Spending): ) members = sorted(group.members, key=lambda x: x.id) # TODO: implement division strategy - # TODO: this truncates results when the amount is not divisible by the number of members + # TODO: this truncates decimals amount_per_member = spending.amount // len(members) txs = [] for user, balance in zip(members, balances): diff --git a/src/main.py b/src/main.py index c7c0682..bd57968 100755 --- a/src/main.py +++ b/src/main.py @@ -7,7 +7,6 @@ from src.mail import mail_service, is_expired_invite from src.database import SessionLocal, engine from sqlalchemy.orm import Session -import hashlib models.Base.metadata.create_all(bind=engine) @@ -451,7 +450,7 @@ def accept_invite(db: DbDependency, user: UserDependency, invite_token: str): if target_group is None or target_group.is_archived: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=f"No se puede agregar un nuevo miembro a este grupo.", + detail="No se puede agregar un nuevo miembro a este grupo.", ) if user_id_in_group(user.id, target_group): diff --git a/src/models.py b/src/models.py index 35d0c4e..7825492 100755 --- a/src/models.py +++ b/src/models.py @@ -8,7 +8,6 @@ Integer, String, Boolean, - Table, UniqueConstraint, func, Enum, diff --git a/src/test_main.py b/src/test_main.py index 0d85ce1..6ba4d8c 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -321,7 +321,7 @@ def test_update_group_non_existant( "description": "TESTING", } response = client.put( - url=f"/group", headers={"x-user": some_credentials.jwt}, json=put_body + url="/group", headers={"x-user": some_credentials.jwt}, json=put_body ) assert response.status_code == HTTPStatus.NOT_FOUND From aa4720e8562aa59fc1905b9c75ba9d6306f19323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 2 Jun 2024 14:02:07 -0300 Subject: [PATCH 05/24] feat: accept emails in add_user_to_group --- src/crud.py | 3 +-- src/main.py | 21 +++++++++++++++++---- src/schemas.py | 4 ++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/crud.py b/src/crud.py index e2c1473..9d81654 100755 --- a/src/crud.py +++ b/src/crud.py @@ -128,8 +128,7 @@ def update_group_status(db: Session, group: models.Group, status: bool): return group -def add_user_to_group(db: Session, user_id: int, group: models.Group): - user = get_user_by_id(db, user_id) +def add_user_to_group(db: Session, user: models.User, group: models.Group): group.members.add(user) db.commit() db.refresh(group) diff --git a/src/main.py b/src/main.py index bd57968..ef93bfd 100755 --- a/src/main.py +++ b/src/main.py @@ -159,12 +159,27 @@ def add_user_to_group( group_id: int, req: schemas.AddUserToGroupRequest, ): + if type(req.user_identifier) == str: + user_to_add = crud.get_user_by_email(db, req.user_identifier) + else: + user_to_add = crud.get_user_by_id(db, req.user_identifier) + + if user_to_add is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Usuario no existe" + ) + group = crud.get_group_by_id(db, group_id) check_group_exists_and_user_is_owner(user.id, group) check_group_is_unarchived(group) + if user_id_in_group(user.id, group): + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"El usuario ya es miembro del grupo {group.name}", + ) - crud.add_user_to_group(db, req.user_id, group) + group = crud.add_user_to_group(db, user_to_add, group) return group.members @@ -390,7 +405,6 @@ def send_invite( mail: MailDependency, invite: schemas.InviteCreate, ): - receiver = crud.get_user_by_email(db, invite.receiver_email) if receiver is None: raise HTTPException( @@ -424,7 +438,6 @@ def send_invite( @app.post("/invite/join/{invite_token}", status_code=HTTPStatus.OK) def accept_invite(db: DbDependency, user: UserDependency, invite_token: str): - target_invite = crud.get_invite_by_token(db, invite_token) if target_invite is None: raise HTTPException( @@ -459,5 +472,5 @@ def accept_invite(db: DbDependency, user: UserDependency, invite_token: str): detail=f"El usuario ya es miembro del grupo {target_group.name}", ) - crud.add_user_to_group(db, user.id, target_group) + crud.add_user_to_group(db, user, target_group) return crud.update_invite_status(db, target_invite, schemas.InviteStatus.ACCEPTED) diff --git a/src/schemas.py b/src/schemas.py index 1849e67..56c91a3 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import StrEnum, auto -from typing import Optional +from typing import Optional, Union from uuid import UUID from pydantic import BaseModel, Field @@ -31,7 +31,7 @@ class UserCredentials(User): class AddUserToGroupRequest(BaseModel): - user_id: int + user_identifier: Union[int, str] ################################################ From 00bc9429c281d883f7260d3c36337c82f8ebcc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:15:19 -0300 Subject: [PATCH 06/24] Remove redundant code as per CR --- src/crud.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/crud.py b/src/crud.py index 9d81654..992e511 100755 --- a/src/crud.py +++ b/src/crud.py @@ -89,8 +89,6 @@ def create_group(db: Session, group: schemas.GroupCreate, user_id: int): # Add the owner to the group members db_user = get_user_by_id(db, user_id) - db.commit() - db.refresh(db_group) db_group.members.add(db_user) db.commit() db.refresh(db_group) From cd1c59a727283941da903adb545f014eacf1fdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:07:39 -0300 Subject: [PATCH 07/24] fix: fetch user from DB instead of jwt --- src/main.py | 19 ++++++++++++------- src/test_main.py | 19 ++++++++----------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/main.py b/src/main.py index ef93bfd..dac763b 100755 --- a/src/main.py +++ b/src/main.py @@ -26,14 +26,19 @@ def get_db(): DbDependency = Annotated[Session, Depends(get_db)] -def ensure_user(x_user: Annotated[str, Header()]) -> models.User: +def ensure_user(db: DbDependency, x_user: Annotated[str, Header()]) -> models.User: jwt_claims = auth.parse_jwt(x_user) if jwt_claims is None: raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, detail="Necesita loguearse para continuar", ) - user = models.User(id=jwt_claims["id"], email=jwt_claims["email"]) + user = crud.get_user_by_id(db, jwt_claims["id"]) + if user is None: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Usuario no encontrado", + ) return user @@ -72,7 +77,7 @@ def login(user: schemas.UserLogin, db: DbDependency) -> schemas.UserCredentials: if not auth.valid_password(user.password, db_user.hashed_password): raise HTTPException( - status_code=HTTPStatus.UNAUTHORIZED, detail="Contraseña incorrecta" + status_code=HTTPStatus.FORBIDDEN, detail="Contraseña incorrecta" ) credentials = auth.login_user(db_user) @@ -116,7 +121,7 @@ def check_group_exists_and_user_is_owner(user_id: int, group: models.Group): # If user is in group, but is not the owner if group.owner_id != user_id: raise HTTPException( - status_code=HTTPStatus.UNAUTHORIZED, + status_code=HTTPStatus.FORBIDDEN, detail="No tiene permisos para modificar este grupo", ) @@ -163,17 +168,17 @@ def add_user_to_group( user_to_add = crud.get_user_by_email(db, req.user_identifier) else: user_to_add = crud.get_user_by_id(db, req.user_identifier) - + if user_to_add is None: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Usuario no existe" ) - + group = crud.get_group_by_id(db, group_id) check_group_exists_and_user_is_owner(user.id, group) check_group_is_unarchived(group) - if user_id_in_group(user.id, group): + if user_id_in_group(user_to_add.id, group): raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"El usuario ya es miembro del grupo {group.name}", diff --git a/src/test_main.py b/src/test_main.py index 6ba4d8c..2755523 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -91,7 +91,7 @@ def some_group_members( response = client.post( url=f"/group/{some_group.id}/member", headers={"x-user": some_credentials.jwt}, - json={"user_id": credentials.id}, + json={"user_identifier": credentials.id}, ) assert response.status_code == HTTPStatus.CREATED @@ -168,7 +168,7 @@ def test_login_with_wrong_password(client: TestClient): json={"email": "example@example.com", "password": "a_wrong_password"}, ) - assert second_response.status_code == HTTPStatus.UNAUTHORIZED + assert second_response.status_code == HTTPStatus.FORBIDDEN assert "jwt" not in second_response.json() @@ -332,21 +332,19 @@ def test_add_user_to_group( some_group: schemas.Group, ): # Create new user - body = {"email": "some_email@example.com", "password": "some_password"} - response = client.post(url="/user/register", json=body) - assert response.status_code == HTTPStatus.CREATED - user = response.json() + new_user = make_user_credentials(client, "some_random_email@email.com") # Add new user to group response = client.post( url=f"/group/{some_group.id}/member", headers={"x-user": some_credentials.jwt}, - json={"user_id": user["id"]}, + json={"user_identifier": new_user.id}, ) + expected_members = sorted([some_credentials.id, new_user.id]) body = response.json() - assert response.status_code == HTTPStatus.CREATED + assert response.status_code == HTTPStatus.CREATED, str(body) assert len(body) == 2 - assert sorted([u["id"] for u in body]) == sorted([some_credentials.id, user["id"]]) + assert sorted([u["id"] for u in body]) == expected_members # GET group members response = client.get( @@ -358,7 +356,7 @@ def test_add_user_to_group( assert response.status_code == HTTPStatus.OK assert len(body) == 2 - assert sorted([u["id"] for u in body]) == sorted([some_credentials.id, user["id"]]) + assert sorted([u["id"] for u in body]) == expected_members ################################################ @@ -690,7 +688,6 @@ def some_invite( some_other_credentials: schemas.UserCredentials, some_group: schemas.Group, ): - # Create Invite response = client.post( url="/invite", From 79a2d77976e96e74b36a03c672acd3465fa9b291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 3 Jun 2024 23:08:49 -0300 Subject: [PATCH 08/24] refactor: use isinstance instead of type --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index dac763b..48fd0da 100755 --- a/src/main.py +++ b/src/main.py @@ -164,7 +164,7 @@ def add_user_to_group( group_id: int, req: schemas.AddUserToGroupRequest, ): - if type(req.user_identifier) == str: + if isinstance(req.user_identifier, str): user_to_add = crud.get_user_by_email(db, req.user_identifier) else: user_to_add = crud.get_user_by_id(db, req.user_identifier) From 3a8dbb0a137b9bb2e15bd162eed7c7e464403767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:28:51 -0300 Subject: [PATCH 09/24] chore: change psycopg2 to psycopg2-binary --- poetry.lock | 667 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 371 insertions(+), 298 deletions(-) diff --git a/poetry.lock b/poetry.lock index 86eaa5f..9c1f842 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,24 +2,24 @@ [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "anyio" -version = "4.3.0" +version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] @@ -77,13 +77,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.2.2" +version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, + {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] [[package]] @@ -177,43 +177,43 @@ files = [ [[package]] name = "cryptography" -version = "42.0.7" +version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, - {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, - {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, - {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, - {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, - {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, - {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, - {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, - {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, - {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, - {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, - {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, - {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, - {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, ] [package.dependencies] @@ -311,19 +311,20 @@ all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)" [[package]] name = "fastapi-cli" -version = "0.0.3" +version = "0.0.4" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_cli-0.0.3-py3-none-any.whl", hash = "sha256:ae233115f729945479044917d949095e829d2d84f56f55ce1ca17627872825a5"}, - {file = "fastapi_cli-0.0.3.tar.gz", hash = "sha256:3b6e4d2c4daee940fb8db59ebbfd60a72c4b962bcf593e263e4cc69da4ea3d7f"}, + {file = "fastapi_cli-0.0.4-py3-none-any.whl", hash = "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809"}, + {file = "fastapi_cli-0.0.4.tar.gz", hash = "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"}, ] [package.dependencies] -fastapi = "*" typer = ">=0.12.3" -uvicorn = {version = ">=0.15.0", extras = ["standard"]} + +[package.extras] +standard = ["fastapi", "uvicorn[standard] (>=0.15.0)"] [[package]] name = "flake8" @@ -760,13 +761,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -790,25 +791,84 @@ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -name = "psycopg2" +name = "psycopg2-binary" version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.7" files = [ - {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, - {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, - {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, - {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, - {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, - {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, - {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, - {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, - {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, - {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, - {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, - {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, - {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, ] [[package]] @@ -846,18 +906,18 @@ files = [ [[package]] name = "pydantic" -version = "2.7.1" +version = "2.7.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, + {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, + {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" +pydantic-core = "2.18.4" typing-extensions = ">=4.6.1" [package.extras] @@ -865,90 +925,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.18.2" +version = "2.18.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, + {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, + {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, + {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, + {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, + {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, + {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, + {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, + {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"}, + {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"}, + {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, + {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, + {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, + {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, ] [package.dependencies] @@ -981,13 +1041,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -1327,87 +1387,100 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "ujson" -version = "5.9.0" +version = "5.10.0" description = "Ultra fast JSON encoder and decoder for Python" optional = false python-versions = ">=3.8" files = [ - {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, - {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, - {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, - {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, - {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, - {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, - {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, - {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, - {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, - {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, - {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, - {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, - {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, + {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, + {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, + {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, + {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, + {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, + {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, + {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, + {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, + {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, + {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, + {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, + {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, + {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, ] [[package]] @@ -1498,86 +1571,86 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "watchfiles" -version = "0.21.0" +version = "0.22.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.8" files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, + {file = "watchfiles-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538"}, + {file = "watchfiles-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d9188979a58a096b6f8090e816ccc3f255f137a009dd4bbec628e27696d67c1"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bdadf6b90c099ca079d468f976fd50062905d61fae183f769637cb0f68ba59a"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:067dea90c43bf837d41e72e546196e674f68c23702d3ef80e4e816937b0a3ffd"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf8a20266136507abf88b0df2328e6a9a7c7309e8daff124dda3803306a9fdb"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1235c11510ea557fe21be5d0e354bae2c655a8ee6519c94617fe63e05bca4171"}, + {file = "watchfiles-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2444dc7cb9d8cc5ab88ebe792a8d75709d96eeef47f4c8fccb6df7c7bc5be71"}, + {file = "watchfiles-0.22.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c5af2347d17ab0bd59366db8752d9e037982e259cacb2ba06f2c41c08af02c39"}, + {file = "watchfiles-0.22.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9624a68b96c878c10437199d9a8b7d7e542feddda8d5ecff58fdc8e67b460848"}, + {file = "watchfiles-0.22.0-cp310-none-win32.whl", hash = "sha256:4b9f2a128a32a2c273d63eb1fdbf49ad64852fc38d15b34eaa3f7ca2f0d2b797"}, + {file = "watchfiles-0.22.0-cp310-none-win_amd64.whl", hash = "sha256:2627a91e8110b8de2406d8b2474427c86f5a62bf7d9ab3654f541f319ef22bcb"}, + {file = "watchfiles-0.22.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8c39987a1397a877217be1ac0fb1d8b9f662c6077b90ff3de2c05f235e6a8f96"}, + {file = "watchfiles-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a927b3034d0672f62fb2ef7ea3c9fc76d063c4b15ea852d1db2dc75fe2c09696"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052d668a167e9fc345c24203b104c313c86654dd6c0feb4b8a6dfc2462239249"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e45fb0d70dda1623a7045bd00c9e036e6f1f6a85e4ef2c8ae602b1dfadf7550"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c49b76a78c156979759d759339fb62eb0549515acfe4fd18bb151cc07366629c"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a65474fd2b4c63e2c18ac67a0c6c66b82f4e73e2e4d940f837ed3d2fd9d4da"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc0cba54f47c660d9fa3218158b8963c517ed23bd9f45fe463f08262a4adae1"}, + {file = "watchfiles-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ebe84a035993bb7668f58a0ebf998174fb723a39e4ef9fce95baabb42b787f"}, + {file = "watchfiles-0.22.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0f0a874231e2839abbf473256efffe577d6ee2e3bfa5b540479e892e47c172d"}, + {file = "watchfiles-0.22.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:213792c2cd3150b903e6e7884d40660e0bcec4465e00563a5fc03f30ea9c166c"}, + {file = "watchfiles-0.22.0-cp311-none-win32.whl", hash = "sha256:b44b70850f0073b5fcc0b31ede8b4e736860d70e2dbf55701e05d3227a154a67"}, + {file = "watchfiles-0.22.0-cp311-none-win_amd64.whl", hash = "sha256:00f39592cdd124b4ec5ed0b1edfae091567c72c7da1487ae645426d1b0ffcad1"}, + {file = "watchfiles-0.22.0-cp311-none-win_arm64.whl", hash = "sha256:3218a6f908f6a276941422b035b511b6d0d8328edd89a53ae8c65be139073f84"}, + {file = "watchfiles-0.22.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c7b978c384e29d6c7372209cbf421d82286a807bbcdeb315427687f8371c340a"}, + {file = "watchfiles-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd4c06100bce70a20c4b81e599e5886cf504c9532951df65ad1133e508bf20be"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:425440e55cd735386ec7925f64d5dde392e69979d4c8459f6bb4e920210407f2"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68fe0c4d22332d7ce53ad094622b27e67440dacefbaedd29e0794d26e247280c"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8a31bfd98f846c3c284ba694c6365620b637debdd36e46e1859c897123aa232"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc2e8fe41f3cac0660197d95216c42910c2b7e9c70d48e6d84e22f577d106fc1"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b7cc10261c2786c41d9207193a85c1db1b725cf87936df40972aab466179b6"}, + {file = "watchfiles-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28585744c931576e535860eaf3f2c0ec7deb68e3b9c5a85ca566d69d36d8dd27"}, + {file = "watchfiles-0.22.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00095dd368f73f8f1c3a7982a9801190cc88a2f3582dd395b289294f8975172b"}, + {file = "watchfiles-0.22.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:52fc9b0dbf54d43301a19b236b4a4614e610605f95e8c3f0f65c3a456ffd7d35"}, + {file = "watchfiles-0.22.0-cp312-none-win32.whl", hash = "sha256:581f0a051ba7bafd03e17127735d92f4d286af941dacf94bcf823b101366249e"}, + {file = "watchfiles-0.22.0-cp312-none-win_amd64.whl", hash = "sha256:aec83c3ba24c723eac14225194b862af176d52292d271c98820199110e31141e"}, + {file = "watchfiles-0.22.0-cp312-none-win_arm64.whl", hash = "sha256:c668228833c5619f6618699a2c12be057711b0ea6396aeaece4ded94184304ea"}, + {file = "watchfiles-0.22.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d47e9ef1a94cc7a536039e46738e17cce058ac1593b2eccdede8bf72e45f372a"}, + {file = "watchfiles-0.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28f393c1194b6eaadcdd8f941307fc9bbd7eb567995232c830f6aef38e8a6e88"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd64f3a4db121bc161644c9e10a9acdb836853155a108c2446db2f5ae1778c3d"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2abeb79209630da981f8ebca30a2c84b4c3516a214451bfc5f106723c5f45843"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cc382083afba7918e32d5ef12321421ef43d685b9a67cc452a6e6e18920890e"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d048ad5d25b363ba1d19f92dcf29023988524bee6f9d952130b316c5802069cb"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:103622865599f8082f03af4214eaff90e2426edff5e8522c8f9e93dc17caee13"}, + {file = "watchfiles-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e1f3cf81f1f823e7874ae563457828e940d75573c8fbf0ee66818c8b6a9099"}, + {file = "watchfiles-0.22.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8597b6f9dc410bdafc8bb362dac1cbc9b4684a8310e16b1ff5eee8725d13dcd6"}, + {file = "watchfiles-0.22.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b04a2cbc30e110303baa6d3ddce8ca3664bc3403be0f0ad513d1843a41c97d1"}, + {file = "watchfiles-0.22.0-cp38-none-win32.whl", hash = "sha256:b610fb5e27825b570554d01cec427b6620ce9bd21ff8ab775fc3a32f28bba63e"}, + {file = "watchfiles-0.22.0-cp38-none-win_amd64.whl", hash = "sha256:fe82d13461418ca5e5a808a9e40f79c1879351fcaeddbede094028e74d836e86"}, + {file = "watchfiles-0.22.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3973145235a38f73c61474d56ad6199124e7488822f3a4fc97c72009751ae3b0"}, + {file = "watchfiles-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:280a4afbc607cdfc9571b9904b03a478fc9f08bbeec382d648181c695648202f"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a0d883351a34c01bd53cfa75cd0292e3f7e268bacf2f9e33af4ecede7e21d1d"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9165bcab15f2b6d90eedc5c20a7f8a03156b3773e5fb06a790b54ccecdb73385"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc1b9b56f051209be458b87edb6856a449ad3f803315d87b2da4c93b43a6fe72"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc1fc25a1dedf2dd952909c8e5cb210791e5f2d9bc5e0e8ebc28dd42fed7562"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc92d2d2706d2b862ce0568b24987eba51e17e14b79a1abcd2edc39e48e743c8"}, + {file = "watchfiles-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97b94e14b88409c58cdf4a8eaf0e67dfd3ece7e9ce7140ea6ff48b0407a593ec"}, + {file = "watchfiles-0.22.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96eec15e5ea7c0b6eb5bfffe990fc7c6bd833acf7e26704eb18387fb2f5fd087"}, + {file = "watchfiles-0.22.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:28324d6b28bcb8d7c1041648d7b63be07a16db5510bea923fc80b91a2a6cbed6"}, + {file = "watchfiles-0.22.0-cp39-none-win32.whl", hash = "sha256:8c3e3675e6e39dc59b8fe5c914a19d30029e36e9f99468dddffd432d8a7b1c93"}, + {file = "watchfiles-0.22.0-cp39-none-win_amd64.whl", hash = "sha256:25c817ff2a86bc3de3ed2df1703e3d24ce03479b27bb4527c57e722f8554d971"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b810a2c7878cbdecca12feae2c2ae8af59bea016a78bc353c184fa1e09f76b68"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7e1f9c5d1160d03b93fc4b68a0aeb82fe25563e12fbcdc8507f8434ab6f823c"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030bc4e68d14bcad2294ff68c1ed87215fbd9a10d9dea74e7cfe8a17869785ab"}, + {file = "watchfiles-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace7d060432acde5532e26863e897ee684780337afb775107c0a90ae8dbccfd2"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5834e1f8b71476a26df97d121c0c0ed3549d869124ed2433e02491553cb468c2"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0bc3b2f93a140df6806c8467c7f51ed5e55a931b031b5c2d7ff6132292e803d6"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fdebb655bb1ba0122402352b0a4254812717a017d2dc49372a1d47e24073795"}, + {file = "watchfiles-0.22.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c8e0aa0e8cc2a43561e0184c0513e291ca891db13a269d8d47cb9841ced7c71"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2f350cbaa4bb812314af5dab0eb8d538481e2e2279472890864547f3fe2281ed"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7a74436c415843af2a769b36bf043b6ccbc0f8d784814ba3d42fc961cdb0a9dc"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00ad0bcd399503a84cc688590cdffbe7a991691314dde5b57b3ed50a41319a31"}, + {file = "watchfiles-0.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a44e9481afc7a5ee3291b09c419abab93b7e9c306c9ef9108cb76728ca58d2"}, + {file = "watchfiles-0.22.0.tar.gz", hash = "sha256:988e981aaab4f3955209e7e28c7794acdb690be1efa7f16f8ea5aba7ffdadacb"}, ] [package.dependencies] @@ -1667,4 +1740,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "b9c425335839c578e4263e2c1814650e59d15120cd3d2f8a6042f34e1bad18a8" +content-hash = "bf22be7163c1b9a98374d2fbf9d457f5f3731bade161cdf34ad253eeed8a92ab" diff --git a/pyproject.toml b/pyproject.toml index 982e71b..8329a6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ fastapi = "^0.111.0" sqlalchemy = "^2.0.30" uvicorn = "^0.29.0" flake8 = "^7.0.0" -psycopg2 = "^2.9.9" +psycopg2-binary = "^2.9.9" python-jose = {extras = ["cryptography"], version = "^3.3.0"} sib-api-v3-sdk = "^7.6.0" From 287779d66a5bbef523c318b6a62db554f8436cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:28:51 -0300 Subject: [PATCH 10/24] feat: add payment endpoints --- .gitignore | 1 + src/crud.py | 26 ++++++++++++++++++++++++-- src/main.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/models.py | 11 +++++++++++ src/schemas.py | 21 +++++++++++++++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b6b4e0f..1cb5fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python *.db +.vscode diff --git a/src/crud.py b/src/crud.py index 992e511..4b61b22 100755 --- a/src/crud.py +++ b/src/crud.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from sqlalchemy import select from sqlalchemy.orm import Session from uuid import UUID @@ -15,7 +15,7 @@ def get_user_by_id(db: Session, id: int): return db.query(models.User).filter(models.User.id == id).first() -def get_user_by_email(db: Session, email: str) -> models.User: +def get_user_by_email(db: Session, email: str) -> Optional[models.User]: return db.query(models.User).filter(models.User.email == email).first() @@ -166,6 +166,28 @@ def get_spendings_by_category(db: Session, category_id: int): ) +################################################ +# PAYMENTS +################################################ + + +def create_payment(db: Session, payment: schemas.PaymentCreate): + db_payment = models.Spending(**dict(payment)) + db.add(db_payment) + db.commit() + db.refresh(db_payment) + return db_payment + + +def get_payments_by_group_id(db: Session, group_id: int): + return ( + db.query(models.Payment) + .filter(models.Payment.group_id == group_id) + .limit(100) + .all() + ) + + ################################################ # BUDGETS ################################################ diff --git a/src/main.py b/src/main.py index 48fd0da..71399bf 100755 --- a/src/main.py +++ b/src/main.py @@ -334,6 +334,55 @@ def list_group_spendings(db: DbDependency, user: UserDependency, group_id: int): return crud.get_spendings_by_group_id(db, group_id) +################################################ +# PAYMENTS +################################################ + + +@app.post("/payment", status_code=HTTPStatus.CREATED) +def create_payment( + payment: schemas.PaymentCreate, db: DbDependency, user: UserDependency +): + group = crud.get_group_by_id(db, payment.group_id) + + # Check creator, sender, and receiver are members of the group + check_group_exists_and_user_is_member(user.id, group) + check_group_exists_and_user_is_member(payment.from_id, group) + check_group_exists_and_user_is_member(payment.to_id, group) + + if payment.from_id == payment.to_id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="No se puede realizar un pago a uno mismo.", + ) + + if group.is_archived: + raise HTTPException( + status_code=HTTPStatus.NOT_ACCEPTABLE, + detail="El grupo esta archivado, no se pueden seguir agregando pagos.", + ) + + return crud.create_payment(db, payment) + + +@app.get("/payment") +def list_payments(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + + check_group_exists_and_user_is_member(user.id, group) + + return crud.get_payments_by_group_id(db, group_id) + + +@app.get("/group/{group_id}/payment") +def list_group_payments(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + + check_group_exists_and_user_is_member(user.id, group) + + return crud.get_payments_by_group_id(db, group_id) + + ################################################ # BUDGETS ################################################ diff --git a/src/models.py b/src/models.py index 7825492..91643f6 100755 --- a/src/models.py +++ b/src/models.py @@ -67,6 +67,17 @@ class Spending(Base): date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) +class Payment(Base): + __tablename__ = "payments" + + id = Column(Integer, primary_key=True) + group_id = Column(ForeignKey("groups.id")) + from_id = Column(ForeignKey("users.id")) + to_id = Column(ForeignKey("users.id")) + amount = Column(Integer) + date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + + class Budget(Base): __tablename__ = "budgets" diff --git a/src/schemas.py b/src/schemas.py index 56c91a3..694050b 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -108,6 +108,27 @@ class Spending(SpendingBase): owner_id: int +################################################ +# PAYMENTS +################################################ + + +class PaymentBase(BaseModel): + group_id: int + from_id: int + to_id: int + amount: int + date: Optional[datetime] = Field(None) + + +class PaymentCreate(PaymentBase): + pass + + +class Payment(PaymentBase): + id: int + + ################################################ # BUDGETS ################################################ From 83aeedcb528cd4038f8ce1716f8a1f63d4dc46bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:28:51 -0300 Subject: [PATCH 11/24] test: add unit tests --- src/crud.py | 2 +- src/test_main.py | 71 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/crud.py b/src/crud.py index 4b61b22..95777ad 100755 --- a/src/crud.py +++ b/src/crud.py @@ -172,7 +172,7 @@ def get_spendings_by_category(db: Session, category_id: int): def create_payment(db: Session, payment: schemas.PaymentCreate): - db_payment = models.Spending(**dict(payment)) + db_payment = models.Payment(**dict(payment)) db.add(db_payment) db.commit() db.refresh(db_payment) diff --git a/src/test_main.py b/src/test_main.py index 2755523..3969aab 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -926,3 +926,74 @@ def test_balance_multiple_members( some_spending.amount if user.id == some_spending.owner_id else 0 ) assert balance["current_balance"] == expected_balance + + +################################################ +# PAYMENTS +################################################ + + +@pytest.fixture +def some_payment( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_other_credentials: schemas.UserCredentials, + some_group: schemas.Group, +): + res = client.post( + url=f"/group/{some_group.id}/member", + json={ + "user_identifier": some_other_credentials.id, + }, + headers={"x-user": some_credentials.jwt}, + ) + assert res.status_code == HTTPStatus.CREATED + + response = client.post( + url="/payment", + json={ + "group_id": some_group.id, + "from_id": some_credentials.id, + "to_id": some_other_credentials.id, + "amount": 500, + }, + headers={"x-user": some_credentials.jwt}, + ) + + assert response.status_code == HTTPStatus.CREATED + response_body = response.json() + assert "id" in response_body + assert response_body["group_id"] == some_group.id + return schemas.Payment(**response_body) + + +def test_create_payment(some_payment: schemas.Payment): + # NOTE: test is inside fixture + pass + + +def test_payment_updates_balance( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_other_credentials: schemas.UserCredentials, + some_payment: schemas.Payment, +): + response = client.get( + url=f"/group/{some_payment.group_id}/balance", + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.OK + + balance_list = response.json() + assert len(balance_list) == 2 + + balance_list.sort(key=lambda x: x["user_id"]) + [some_balance, some_other_balance] = balance_list + + assert some_balance["user_id"] == some_credentials.id + assert some_balance["group_id"] == some_payment.group_id + assert some_balance["current_balance"] == some_payment.amount + + assert some_other_balance["user_id"] == some_other_credentials.id + assert some_other_balance["group_id"] == some_payment.group_id + assert some_other_balance["current_balance"] == -some_payment.amount From 701ae1a11a93786e0a37573603a15771b1009349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:28:51 -0300 Subject: [PATCH 12/24] chore: remove transactions from model --- src/crud.py | 33 ++++++++++++++++++--------------- src/models.py | 13 ------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/crud.py b/src/crud.py index 95777ad..dd25b0f 100755 --- a/src/crud.py +++ b/src/crud.py @@ -143,7 +143,7 @@ def create_spending(db: Session, spending: schemas.SpendingCreate, user_id: int) db.add(db_spending) db.commit() db.refresh(db_spending) - create_transactions_from_spending(db, db_spending) + update_balances_from_spending(db, db_spending) db.refresh(db_spending) return db_spending @@ -173,6 +173,7 @@ def get_spendings_by_category(db: Session, category_id: int): def create_payment(db: Session, payment: schemas.PaymentCreate): db_payment = models.Payment(**dict(payment)) + update_balances_from_payment(db, db_payment) db.add(db_payment) db.commit() db.refresh(db_payment) @@ -260,11 +261,11 @@ def update_invite_status( ################################################ -# TRANSACTIONS +# BALANCES ################################################ -def create_transactions_from_spending(db: Session, spending: models.Spending): +def update_balances_from_spending(db: Session, spending: models.Spending): group = get_group_by_id(db, spending.group_id) balances = sorted( get_balances_by_group_id(db, spending.group_id), key=lambda x: x.user_id @@ -273,29 +274,31 @@ def create_transactions_from_spending(db: Session, spending: models.Spending): # TODO: implement division strategy # TODO: this truncates decimals amount_per_member = spending.amount // len(members) - txs = [] for user, balance in zip(members, balances): amount = -amount_per_member if spending.owner_id == user.id: amount += spending.amount - tx = models.Transaction( - from_user_id=spending.owner_id, - to_user_id=user.id, - amount=amount, - spending_id=spending.id, - ) - txs.append(tx) - db.add(tx) balance.current_balance += amount db.commit() -################################################ -# BALANCES -################################################ +def update_balances_from_payment(db: Session, payment: models.Payment): + balances = get_balances_by_group_id(db, payment.group_id) + + # Update payer balance + payer = get_user_by_id(db, payment.from_id) + payer_balance = next(filter(lambda x: x.user_id == payer.id, balances)) + payer_balance.current_balance += payment.amount + + # Update payee balance + payee = get_user_by_id(db, payment.to_id) + payee_balance = next(filter(lambda x: x.user_id == payee.id, balances)) + payee_balance.current_balance -= payment.amount + + db.commit() def get_balances_by_group_id(db: Session, group_id: int) -> List[models.Balance]: diff --git a/src/models.py b/src/models.py index 91643f6..734ab50 100755 --- a/src/models.py +++ b/src/models.py @@ -102,19 +102,6 @@ class Invite(Base): creation_date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) -class Transaction(Base): - __tablename__ = "transactions" - - id = Column(Integer, primary_key=True) - spending_id = Column(ForeignKey("spendings.id")) - from_user_id = Column(ForeignKey("users.id")) - to_user_id = Column(ForeignKey("users.id")) - date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) - amount = Column(Integer) - - __table_args__ = (UniqueConstraint("spending_id", "to_user_id"),) - - class Balance(Base): __tablename__ = "balances" From 12dcce863bcad7bb8f0d14d6ae4c7537f5a523cd Mon Sep 17 00:00:00 2001 From: danielaojeda1 Date: Sun, 9 Jun 2024 21:51:57 -0300 Subject: [PATCH 13/24] WIP: falta testeo local y creacion de tests --- src/crud.py | 12 ++++++++++++ src/mail.py | 33 +++++++++++++++++++++++++++++++++ src/main.py | 27 +++++++++++++++++++++++++++ src/models.py | 11 +++++++++++ src/schemas.py | 17 +++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/src/crud.py b/src/crud.py index 992e511..4f7ff9a 100755 --- a/src/crud.py +++ b/src/crud.py @@ -236,6 +236,18 @@ def update_invite_status( db.refresh(db_invite) return db_invite +################################################ +# REMINDERS +################################################ + +def create_payment_reminder(db: Session, payment_reminder: schemas.PaymentReminderCreate, sender_id: int): + db_reminder = models.PaymentReminder(sender_id=sender_id, + receiver_id=payment_reminder.receiver_id, + group_id=payment_reminder.group_id) + db.add(db_reminder) + db.commit() + db.refresh(db_reminder) + return db_reminder ################################################ # TRANSACTIONS diff --git a/src/mail.py b/src/mail.py index 4d1e61f..2354da8 100644 --- a/src/mail.py +++ b/src/mail.py @@ -17,6 +17,10 @@ class MailSender(ABC): def send(self, sender: str, receiver: str, group_name: str) -> bool: pass + @abstractmethod + def send_reminder(self, sender: str, receiver: str, group_id: int) -> bool: + pass + class ProdMailSender(MailSender): def send( @@ -44,6 +48,30 @@ def send( except ApiException as e: error(f"Failed to send email with error: {e}") return False + + def send_reminder( + self, sender: str, receiver: str, group: schemas.Group) -> bool: + configuration = sdk.Configuration() + configuration.api_key["api-key"] = API_KEY + + api_instance = sdk.TransactionalEmailsApi(sdk.ApiClient(configuration)) + + to = [{"email": receiver}] + params = { + "sender": sender, + "group_id": group.id, + "group_name": group.name, + } + + email = sdk.SendSmtpEmail(to=to, template_id=TEMPLATE_ID, params=params) + + try: + response = api_instance.send_transac_email(email) + info(response) + return True + except ApiException as e: + error(f"Failed to send email with error: {e}") + return False class LocalMailSender(MailSender): @@ -51,6 +79,11 @@ def send( self, sender: str, receiver: str, group: schemas.Group, token: str ) -> bool: return True + + def send_reminder( + self, sender: str, receiver: str, group: schemas.Group, token: str + ) -> bool: + return True if API_KEY is not None: diff --git a/src/main.py b/src/main.py index 48fd0da..e74e962 100755 --- a/src/main.py +++ b/src/main.py @@ -479,3 +479,30 @@ def accept_invite(db: DbDependency, user: UserDependency, invite_token: str): crud.add_user_to_group(db, user, target_group) return crud.update_invite_status(db, target_invite, schemas.InviteStatus.ACCEPTED) + +################################################ +# REMINDERS +################################################ + +@app.post("/payment_reminder", status_code=HTTPStatus.CREATED) +def send_payment_reminder(db: DbDependency, + user: UserDependency, + mail: MailDependency, + payment_reminder: schemas.PaymentReminderCreate): + + receiver = crud.get_user_by_email(db, payment_reminder.receiver_email) + group = crud.get_group_by_id(db, payment_reminder.group_id) + check_group_exists_and_user_is_member(receiver.id, group) + check_group_is_unarchived(group) + payment_reminder.receiver_id = receiver.id + + + sent_ok = mail.send_reminder( + sender=user.email, receiver=receiver.email, group=group) + + if sent_ok: + return crud.create_payment_reminder(db, payment_reminder, user.id) + else: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="No se pudo enviar recordatorio de pago al usuario." + ) \ No newline at end of file diff --git a/src/models.py b/src/models.py index 7825492..7261c75 100755 --- a/src/models.py +++ b/src/models.py @@ -113,3 +113,14 @@ class Balance(Base): current_balance = Column(Integer, default=0) __table_args__ = (UniqueConstraint("user_id", "group_id"),) + +class PaymentReminder(Base): + __tablename__ = "payment_reminders" + + id = Column(Integer, primary_key=True, index=True) + sender_id = Column(Integer, ForeignKey("users.id")) + receiver_id = Column(Integer, ForeignKey("users.id")) + group_id = Column(Integer, ForeignKey("groups.id")) + message = Column(String) + creation_date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + diff --git a/src/schemas.py b/src/schemas.py index 56c91a3..416f7fd 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -160,3 +160,20 @@ class Invite(InviteBase): id: int sender_id: int status: InviteStatus + +################################################ +# REMINDERS +################################################ + +class PaymentReminderBase(BaseModel): + creation_date: Optional[datetime] = Field(None) + receiver_id: int + group_id: int + message: Optional[str] = Field(None) + +class PaymentReminderCreate(PaymentReminderBase): + receiver_email: str + +class PaymentReminder(PaymentReminderBase): + id: int + sender_id: int \ No newline at end of file From a6328774f3e6ac8169e7d55527cf2173f4381a6e Mon Sep 17 00:00:00 2001 From: danielaojeda1 Date: Tue, 11 Jun 2024 18:17:26 -0300 Subject: [PATCH 14/24] Solicitar recordatorio de pago listo, solo falta make test --- src/mail.py | 3 +-- src/main.py | 4 ++++ src/test_main.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/mail.py b/src/mail.py index 2354da8..44524ad 100644 --- a/src/mail.py +++ b/src/mail.py @@ -81,8 +81,7 @@ def send( return True def send_reminder( - self, sender: str, receiver: str, group: schemas.Group, token: str - ) -> bool: + self, sender: str, receiver: str, group: schemas.Group) -> bool: return True diff --git a/src/main.py b/src/main.py index e74e962..7aac1a4 100755 --- a/src/main.py +++ b/src/main.py @@ -491,6 +491,10 @@ def send_payment_reminder(db: DbDependency, payment_reminder: schemas.PaymentReminderCreate): receiver = crud.get_user_by_email(db, payment_reminder.receiver_email) + if receiver is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="No se encontro el usuario receptor." + ) group = crud.get_group_by_id(db, payment_reminder.group_id) check_group_exists_and_user_is_member(receiver.id, group) check_group_is_unarchived(group) diff --git a/src/test_main.py b/src/test_main.py index 2755523..b4978ce 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -926,3 +926,54 @@ def test_balance_multiple_members( some_spending.amount if user.id == some_spending.owner_id else 0 ) assert balance["current_balance"] == expected_balance + +################################################ +# PAYMENT REMINDERS +################################################ +def some_payment_reminder( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_other_credentials: schemas.UserCredentials, + some_group: schemas.Group, +): + # Create PaymentReminder + response = client.post( + url="/payment_reminder", + json={ + "receiver_email": some_other_credentials.email, + "group_id": some_group.id, + }, + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.CREATED + response_body = response.json() + + assert "creation_date" in response_body + assert response_body["group_id"] == some_group.id + assert response_body["sender_id"] == some_credentials.id + assert response_body["receiver_id"] == some_other_credentials.id + + return schemas.PaymentReminder(**response_body) + +def test_send_payment_reminder_to_non_registered_user( + client: TestClient, + some_credentials: schemas.UserCredentials, + some_group: schemas.Group, +): + response = client.post( + url="/payment_reminder", + json={"receiver_email": "pepe@gmail.com", "group_id": some_group.id}, + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.NOT_FOUND + +def test_send_payment_reminder_on_non_existant_group( + client: TestClient, some_credentials: schemas.UserCredentials +): + response = client.post( + url="/payment_reminder", + json={"receiver_email": some_credentials.email, "group_id": 12345}, + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.NOT_FOUND + From 71c9d929b03634f23af22a393518a2f8893fb914 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Tue, 11 Jun 2024 21:12:53 -0300 Subject: [PATCH 15/24] Fix: tests and email templates --- src/mail.py | 13 +++++++------ src/main.py | 2 +- src/schemas.py | 2 +- src/test_main.py | 7 +++++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/mail.py b/src/mail.py index 44524ad..6a278a4 100644 --- a/src/mail.py +++ b/src/mail.py @@ -9,12 +9,13 @@ BASE_URL = os.environ.get("BASE_URL", "http://localhost:3000") API_KEY = os.environ.get("EMAIL_API_KEY") -TEMPLATE_ID = 1 +INVITE_TEMPLATE_ID = 1 +REMINDER_TEMPLATE_ID = 2 class MailSender(ABC): @abstractmethod - def send(self, sender: str, receiver: str, group_name: str) -> bool: + def send_invite(self, sender: str, receiver: str, group_name: str) -> bool: pass @abstractmethod @@ -23,7 +24,7 @@ def send_reminder(self, sender: str, receiver: str, group_id: int) -> bool: class ProdMailSender(MailSender): - def send( + def send_invite( self, sender: str, receiver: str, group: schemas.Group, token: str ) -> bool: configuration = sdk.Configuration() @@ -39,7 +40,7 @@ def send( "join_link": f"{BASE_URL}/invites/accept/{token}", } - email = sdk.SendSmtpEmail(to=to, template_id=TEMPLATE_ID, params=params) + email = sdk.SendSmtpEmail(to=to, template_id=INVITE_TEMPLATE_ID, params=params) try: response = api_instance.send_transac_email(email) @@ -63,7 +64,7 @@ def send_reminder( "group_name": group.name, } - email = sdk.SendSmtpEmail(to=to, template_id=TEMPLATE_ID, params=params) + email = sdk.SendSmtpEmail(to=to, template_id=REMINDER_TEMPLATE_ID, params=params) try: response = api_instance.send_transac_email(email) @@ -75,7 +76,7 @@ def send_reminder( class LocalMailSender(MailSender): - def send( + def send_invite( self, sender: str, receiver: str, group: schemas.Group, token: str ) -> bool: return True diff --git a/src/main.py b/src/main.py index 7aac1a4..50b643c 100755 --- a/src/main.py +++ b/src/main.py @@ -429,7 +429,7 @@ def send_invite( ) token = uuid4() - sent_ok = mail.send( + sent_ok = mail.send_invite( sender=user.email, receiver=receiver.email, group=target_group, token=token.hex ) diff --git a/src/schemas.py b/src/schemas.py index 416f7fd..cb682da 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -167,7 +167,7 @@ class Invite(InviteBase): class PaymentReminderBase(BaseModel): creation_date: Optional[datetime] = Field(None) - receiver_id: int + receiver_id: Optional[int] = Field(None) group_id: int message: Optional[str] = Field(None) diff --git a/src/test_main.py b/src/test_main.py index b4978ce..3f39ec6 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -958,11 +958,14 @@ def some_payment_reminder( def test_send_payment_reminder_to_non_registered_user( client: TestClient, some_credentials: schemas.UserCredentials, - some_group: schemas.Group, + some_group: schemas.Group ): response = client.post( url="/payment_reminder", - json={"receiver_email": "pepe@gmail.com", "group_id": some_group.id}, + json={ + "receiver_email": "pepe@gmail.com", + "group_id": some_group.id, + }, headers={"x-user": some_credentials.jwt}, ) assert response.status_code == HTTPStatus.NOT_FOUND From b3ff92ac53cada4dd97e614dfe08bfd414f933d6 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Tue, 11 Jun 2024 21:55:15 -0300 Subject: [PATCH 16/24] Tests and email tinkering --- src/mail.py | 8 +++++--- src/test_main.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/mail.py b/src/mail.py index 6a278a4..3e6319c 100644 --- a/src/mail.py +++ b/src/mail.py @@ -9,9 +9,10 @@ BASE_URL = os.environ.get("BASE_URL", "http://localhost:3000") API_KEY = os.environ.get("EMAIL_API_KEY") + INVITE_TEMPLATE_ID = 1 REMINDER_TEMPLATE_ID = 2 - +DEFAULT_REMINDER = "No demores muucho con la deuda! :D" class MailSender(ABC): @abstractmethod @@ -51,7 +52,7 @@ def send_invite( return False def send_reminder( - self, sender: str, receiver: str, group: schemas.Group) -> bool: + self, sender: str, receiver: str, group: schemas.Group, message: str = DEFAULT_REMINDER) -> bool: configuration = sdk.Configuration() configuration.api_key["api-key"] = API_KEY @@ -60,7 +61,8 @@ def send_reminder( to = [{"email": receiver}] params = { "sender": sender, - "group_id": group.id, + "message": message, + "landing_page": f"{BASE_URL}", "group_name": group.name, } diff --git a/src/test_main.py b/src/test_main.py index 3f39ec6..6be2979 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -48,6 +48,14 @@ def make_user_credentials(client: TestClient, email: str): assert response.status_code == HTTPStatus.CREATED return schemas.UserCredentials(**response.json()) +def add_user_to_group(client: TestClient, group_id: int, new_member_id: int, credentials: schemas.UserCredentials): + response = client.post( + url=f"/group/{group_id}/member", + headers={"x-user": credentials.jwt}, + json={"user_identifier": new_member_id}, + ) + assert response.status_code == HTTPStatus.CREATED + @pytest.fixture() def some_credentials(client: TestClient) -> schemas.UserCredentials: @@ -930,12 +938,15 @@ def test_balance_multiple_members( ################################################ # PAYMENT REMINDERS ################################################ +@pytest.fixture def some_payment_reminder( client: TestClient, some_credentials: schemas.UserCredentials, some_other_credentials: schemas.UserCredentials, some_group: schemas.Group, ): + add_user_to_group(client, some_group.id, some_other_credentials.id, some_credentials) + # Create PaymentReminder response = client.post( url="/payment_reminder", @@ -955,6 +966,11 @@ def some_payment_reminder( return schemas.PaymentReminder(**response_body) +def test_send_reminder(client: TestClient, some_payment_reminder: schemas.PaymentReminder): + # NOTE: test is inside fixture + pass + + def test_send_payment_reminder_to_non_registered_user( client: TestClient, some_credentials: schemas.UserCredentials, @@ -980,3 +996,33 @@ def test_send_payment_reminder_on_non_existant_group( ) assert response.status_code == HTTPStatus.NOT_FOUND +def test_send_reminder_to_non_member(client: TestClient, some_credentials: schemas.UserCredentials, some_group: schemas.Group): + + new_user = make_user_credentials(client, "pepitoelmascapo@gmail.com") + + response = client.post( + url="/payment_reminder", + json={"receiver_email": new_user.email, "group_id": some_group.id}, + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.NOT_FOUND + +def test_send_reminder_to_archived_group(client: TestClient, + some_credentials: schemas.UserCredentials, + some_other_credentials: schemas.UserCredentials, + some_group: schemas.Group): + + add_user_to_group(client, some_group.id, some_other_credentials.id, some_credentials) + + response = client.put( + url=f"/group/{some_group.id}/archive", headers={"x-user": some_credentials.jwt} + ) + assert response.status_code == HTTPStatus.OK + + + response = client.post( + url="/payment_reminder", + json={"receiver_email": some_other_credentials.email, "group_id": some_group.id}, + headers={"x-user": some_credentials.jwt}, + ) + assert response.status_code == HTTPStatus.NOT_ACCEPTABLE From 6024d16a2e3aba468ece7ef3ad98aa12f76d8d00 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Tue, 11 Jun 2024 22:08:47 -0300 Subject: [PATCH 17/24] Final touches --- src/crud.py | 4 ++++ src/mail.py | 6 +++--- src/main.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/crud.py b/src/crud.py index 4f7ff9a..403145e 100755 --- a/src/crud.py +++ b/src/crud.py @@ -244,6 +244,10 @@ def create_payment_reminder(db: Session, payment_reminder: schemas.PaymentRemind db_reminder = models.PaymentReminder(sender_id=sender_id, receiver_id=payment_reminder.receiver_id, group_id=payment_reminder.group_id) + + if payment_reminder.message is not None: + db_reminder.message = payment_reminder.message + db.add(db_reminder) db.commit() db.refresh(db_reminder) diff --git a/src/mail.py b/src/mail.py index 3e6319c..a3da23a 100644 --- a/src/mail.py +++ b/src/mail.py @@ -52,7 +52,7 @@ def send_invite( return False def send_reminder( - self, sender: str, receiver: str, group: schemas.Group, message: str = DEFAULT_REMINDER) -> bool: + self, sender: str, receiver: str, group: schemas.Group, message: str) -> bool: configuration = sdk.Configuration() configuration.api_key["api-key"] = API_KEY @@ -61,7 +61,7 @@ def send_reminder( to = [{"email": receiver}] params = { "sender": sender, - "message": message, + "message": DEFAULT_REMINDER if message is None else message, "landing_page": f"{BASE_URL}", "group_name": group.name, } @@ -84,7 +84,7 @@ def send_invite( return True def send_reminder( - self, sender: str, receiver: str, group: schemas.Group) -> bool: + self, sender: str, receiver: str, group: schemas.Group, message: str) -> bool: return True diff --git a/src/main.py b/src/main.py index 50b643c..b3cfd91 100755 --- a/src/main.py +++ b/src/main.py @@ -502,7 +502,7 @@ def send_payment_reminder(db: DbDependency, sent_ok = mail.send_reminder( - sender=user.email, receiver=receiver.email, group=group) + sender=user.email, receiver=receiver.email, group=group, message=payment_reminder.message) if sent_ok: return crud.create_payment_reminder(db, payment_reminder, user.id) From 145a1d89e29d5fd9b77f71349296ffba0cce29de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 11 Jun 2024 23:28:55 -0300 Subject: [PATCH 18/24] Update MailDependency type --- src/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.py b/src/main.py index b3cfd91..1815200 100755 --- a/src/main.py +++ b/src/main.py @@ -4,7 +4,7 @@ from fastapi import Depends, FastAPI, HTTPException, Header from src import crud, models, schemas, auth -from src.mail import mail_service, is_expired_invite +from src.mail import MailSender, mail_service, is_expired_invite from src.database import SessionLocal, engine from sqlalchemy.orm import Session @@ -45,7 +45,7 @@ def ensure_user(db: DbDependency, x_user: Annotated[str, Header()]) -> models.Us app = FastAPI(dependencies=[Depends(get_db)]) UserDependency = Annotated[models.User, Depends(ensure_user)] -MailDependency = Annotated[models.Invite, Depends(get_mail_sender)] +MailDependency = Annotated[MailSender, Depends(get_mail_sender)] ################################################ # USERS @@ -509,4 +509,4 @@ def send_payment_reminder(db: DbDependency, else: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail="No se pudo enviar recordatorio de pago al usuario." - ) \ No newline at end of file + ) From 84cc4aeec3ed15e03602d25f8ede213ab2276051 Mon Sep 17 00:00:00 2001 From: gabokatta Date: Tue, 11 Jun 2024 23:34:11 -0300 Subject: [PATCH 19/24] typing for send_reminder --- src/mail.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/mail.py b/src/mail.py index a3da23a..00dee51 100644 --- a/src/mail.py +++ b/src/mail.py @@ -2,6 +2,7 @@ from datetime import datetime import os from logging import info, error, warning +from typing import Optional import sib_api_v3_sdk as sdk from sib_api_v3_sdk.rest import ApiException @@ -20,7 +21,7 @@ def send_invite(self, sender: str, receiver: str, group_name: str) -> bool: pass @abstractmethod - def send_reminder(self, sender: str, receiver: str, group_id: int) -> bool: + def send_reminder(self, sender: str, receiver: str, group_id: int, message: Optional[str]) -> bool: pass @@ -52,7 +53,7 @@ def send_invite( return False def send_reminder( - self, sender: str, receiver: str, group: schemas.Group, message: str) -> bool: + self, sender: str, receiver: str, group: schemas.Group, message: Optional[str]) -> bool: configuration = sdk.Configuration() configuration.api_key["api-key"] = API_KEY @@ -84,7 +85,7 @@ def send_invite( return True def send_reminder( - self, sender: str, receiver: str, group: schemas.Group, message: str) -> bool: + self, sender: str, receiver: str, group: schemas.Group, message: Optional[str]) -> bool: return True From 587d5d101a1ded97fbb38f12ff20a8c9d5cf13bc Mon Sep 17 00:00:00 2001 From: ovraulin Date: Wed, 12 Jun 2024 14:05:05 -0300 Subject: [PATCH 20/24] Add unique spendings, installment spendings and recurring spendings --- src/crud.py | 86 +++++++++++++++++++++++++++++++++----- src/main.py | 110 ++++++++++++++++++++++++++++++++++++++++++++----- src/models.py | 30 ++++++++++++-- src/schemas.py | 60 ++++++++++++++++++++++++--- 4 files changed, 258 insertions(+), 28 deletions(-) diff --git a/src/crud.py b/src/crud.py index 992e511..7ec033a 100755 --- a/src/crud.py +++ b/src/crud.py @@ -134,12 +134,27 @@ def add_user_to_group(db: Session, user: models.User, group: models.Group): ################################################ -# SPENDINGS +# ALL SPENDINGS ################################################ +def get_all_spendings_by_group_id(db: Session, group_id: int): + + unique_spendings = db.query(models.UniqueSpending).filter(models.UniqueSpending.group_id == group_id).limit(100).all() + installment_spendings = db.query(models.InstallmentSpending).filter(models.InstallmentSpending.group_id == group_id).limit(100).all() + recurring_spendings = db.query(models.RecurringSpending).filter(models.RecurringSpending.group_id == group_id).limit(100).all() -def create_spending(db: Session, spending: schemas.SpendingCreate, user_id: int): - db_spending = models.Spending(owner_id=user_id, **dict(spending)) + unique_spendings = list(map(lambda spending: {**spending.__dict__, "type": "unique_spending"}, unique_spendings)) + installment_spendings = list(map(lambda spending: {**spending.__dict__, "type": "installment_spending"}, installment_spendings)) + recurring_spendings = list(map(lambda spending: {**spending.__dict__, "type": "recurring_spending"}, recurring_spendings)) + + return unique_spendings + installment_spendings + recurring_spendings + +################################################ +# UNIQUE SPENDINGS +################################################ + +def create_unique_spending(db: Session, spending: schemas.SpendingCreate, user_id: int): + db_spending = models.UniqueSpending(owner_id=user_id, **dict(spending)) db.add(db_spending) db.commit() db.refresh(db_spending) @@ -148,24 +163,75 @@ def create_spending(db: Session, spending: schemas.SpendingCreate, user_id: int) return db_spending -def get_spendings_by_group_id(db: Session, group_id: int): +def get_unique_spendings_by_group_id(db: Session, group_id: int): return ( - db.query(models.Spending) - .filter(models.Spending.group_id == group_id) + db.query(models.UniqueSpending) + .filter(models.UniqueSpending.group_id == group_id) .limit(100) .all() ) -def get_spendings_by_category(db: Session, category_id: int): +################################################ +# INSTALLMENT SPENDINGS +################################################ + + +def create_installment_spending(db: Session, spending: schemas.InstallmentSpendingCreate, user_id: int): + db_spending = models.InstallmentSpending(owner_id=user_id, **dict(spending)) + db.add(db_spending) + db.commit() + db.refresh(db_spending) + # create_transactions_from_spending(db, db_spending) + db.refresh(db_spending) + return db_spending + + +def get_installment_spendings_by_group_id(db: Session, group_id: int): return ( - db.query(models.Spending) - .filter(models.Spending.category_id == category_id) + db.query(models.InstallmentSpending) + .filter(models.InstallmentSpending.group_id == group_id) .limit(100) .all() ) +################################################ +# RECURRING SPENDINGS +################################################ + + +def create_recurring_spending(db: Session, spending: schemas.RecurringSpendingBase, user_id: int): + db_spending = models.RecurringSpending(owner_id=user_id, **dict(spending)) + db.add(db_spending) + db.commit() + db.refresh(db_spending) + # create_transactions_from_spending(db, db_spending) + db.refresh(db_spending) + return db_spending + + +def get_recurring_spendings_by_id(db: Session, recurring_spendig_id: int): + return db.query(models.RecurringSpending).filter(models.RecurringSpending.id == recurring_spendig_id).first() + + +def get_recurring_spendings_by_group_id(db: Session, group_id: int): + return ( + db.query(models.RecurringSpending) + .filter(models.RecurringSpending.group_id == group_id) + .limit(100) + .all() + ) + + +def put_recurring_spendings(db: Session, db_recurring_spending: models.RecurringSpending, put_recurring_spending: schemas.RecurringSpendingPut): + db_recurring_spending.amount = put_recurring_spending.amount + db_recurring_spending.description = put_recurring_spending.description + db_recurring_spending.category_id = put_recurring_spending.categiry_id + db.commit() + db.refresh(db_recurring_spending) + return db_recurring_spending + ################################################ # BUDGETS ################################################ @@ -242,7 +308,7 @@ def update_invite_status( ################################################ -def create_transactions_from_spending(db: Session, spending: models.Spending): +def create_transactions_from_spending(db: Session, spending: models.UniqueSpending | models.InstallmentSpending | models.RecurringSpending): group = get_group_by_id(db, spending.group_id) balances = sorted( get_balances_by_group_id(db, spending.group_id), key=lambda x: x.user_id diff --git a/src/main.py b/src/main.py index 48fd0da..26523f5 100755 --- a/src/main.py +++ b/src/main.py @@ -292,14 +292,26 @@ def list_group_categories(db: DbDependency, user: UserDependency, group_id: int) categories = crud.get_categories_by_group_id(db, group_id) return categories +################################################ +# ALL SPENDINGS +################################################ + +@app.get("/group/{group_id}/spending") +def list_group_unique_spendings(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + + check_group_exists_and_user_is_member(user.id, group) + + return crud.get_all_spendings_by_group_id(db, group_id) + ################################################ -# SPENDINGS +# UNIQUE SPENDINGS ################################################ -@app.post("/spending", status_code=HTTPStatus.CREATED) -def create_spending( +@app.post("/unique-spending", status_code=HTTPStatus.CREATED) +def create_unique_spending( spending: schemas.SpendingCreate, db: DbDependency, user: UserDependency ): group = crud.get_group_by_id(db, spending.group_id) @@ -313,25 +325,103 @@ def create_spending( status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" ) - return crud.create_spending(db, spending, user.id) + return crud.create_unique_spending(db, spending, user.id) -@app.get("/spending") -def list_spendings(db: DbDependency, user: UserDependency, group_id: int): +@app.get("/group/{group_id}/unique-spending") +def list_group_unique_spendings(db: DbDependency, user: UserDependency, group_id: int): group = crud.get_group_by_id(db, group_id) check_group_exists_and_user_is_member(user.id, group) - return crud.get_spendings_by_group_id(db, group_id) + return crud.get_unique_spendings_by_group_id(db, group_id) -@app.get("/group/{group_id}/spending") -def list_group_spendings(db: DbDependency, user: UserDependency, group_id: int): +################################################ +# INSTALLMENT SPENDINGS +################################################ + + +@app.post("/installment-spending", status_code=HTTPStatus.CREATED) +def create_installment_spending( + spending: schemas.InstallmentSpendingCreate, db: DbDependency, user: UserDependency +): + group = crud.get_group_by_id(db, spending.group_id) + + check_group_exists_and_user_is_member(user.id, group) + check_group_is_unarchived(group) + + category = crud.get_category_by_id(db, spending.category_id) + if category is None or category.group_id != spending.group_id: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" + ) + + return crud.create_installment_spending(db, spending, user.id) + + +@app.get("/group/{group_id}/installment-spending") +def list_group_installment_spendings(db: DbDependency, user: UserDependency, group_id: int): + group = crud.get_group_by_id(db, group_id) + + check_group_exists_and_user_is_member(user.id, group) + + return crud.get_installment_spendings_by_group_id(db, group_id) + + + +################################################ +# RECURRING SPENDINGS +################################################ + + +@app.post("/recurring-spending", status_code=HTTPStatus.CREATED) +def create_recurring_spending( + spending: schemas.RecurringSpendingCreate, db: DbDependency, user: UserDependency +): + group = crud.get_group_by_id(db, spending.group_id) + + check_group_exists_and_user_is_member(user.id, group) + check_group_is_unarchived(group) + + category = crud.get_category_by_id(db, spending.category_id) + if category is None or category.group_id != spending.group_id: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" + ) + + return crud.create_recurring_spending(db, spending, user.id) + + +@app.get("/group/{group_id}/recurring-spending") +def list_group_recurring_spendings(db: DbDependency, user: UserDependency, group_id: int): group = crud.get_group_by_id(db, group_id) check_group_exists_and_user_is_member(user.id, group) - return crud.get_spendings_by_group_id(db, group_id) + return crud.get_recurring_spendings_by_group_id(db, group_id) + +@app.put("/recurring-spending/{recurring_spending_id}") +def put_recurring_spendings( + db: DbDependency, + user: UserDependency, + recurring_spending_id: int, + put_recurring_spending: schemas.RecurringSpendingPut, +): + + db_recurring_spending = crud.get_recurring_spendings_by_id(db, recurring_spending_id) + + if db_recurring_spending is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Gasto recurrente inexistente" + ) + + group = crud.get_group_by_id(db, db_recurring_spending.group_id) + + check_group_exists_and_user_is_member(user.id, group) + check_group_is_unarchived(group) + + return crud.put_recurring_spendings(db, db_recurring_spending, put_recurring_spending) ################################################ diff --git a/src/models.py b/src/models.py index 7825492..4ad4e0d 100755 --- a/src/models.py +++ b/src/models.py @@ -54,9 +54,33 @@ class Category(Base): __table_args__ = (UniqueConstraint("group_id", "name"),) +class UniqueSpending(Base): + __tablename__ = "unique_spendings" -class Spending(Base): - __tablename__ = "spendings" + id = Column(Integer, primary_key=True) + owner_id = Column(ForeignKey("users.id")) + group_id = Column(ForeignKey("groups.id")) + category_id = Column(ForeignKey("categories.id")) + amount = Column(Integer) + description = Column(String) + date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + + +class InstallmentSpending(Base): + __tablename__ = "installment_spendings" + + id = Column(Integer, primary_key=True) + owner_id = Column(ForeignKey("users.id")) + group_id = Column(ForeignKey("groups.id")) + category_id = Column(ForeignKey("categories.id")) + amount = Column(Integer) + description = Column(String) + amount_of_installments = Column(Integer) + current_installment = Column(Integer) + date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) + +class RecurringSpending(Base): + __tablename__ = "recurring_spendings" id = Column(Integer, primary_key=True) owner_id = Column(ForeignKey("users.id")) @@ -95,7 +119,7 @@ class Transaction(Base): __tablename__ = "transactions" id = Column(Integer, primary_key=True) - spending_id = Column(ForeignKey("spendings.id")) + spending_id = Column(ForeignKey("unique_spendings.id")) from_user_id = Column(ForeignKey("users.id")) to_user_id = Column(ForeignKey("users.id")) date: Mapped[datetime] = mapped_column(DateTime, default=func.now()) diff --git a/src/schemas.py b/src/schemas.py index 56c91a3..b56ca83 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -83,11 +83,11 @@ class Group(GroupBase): ################################################ -# SPENDINGS +# UNIQUE SPENDINGS ################################################ -class SpendingBase(BaseModel): +class UniqueSpendingBase(BaseModel): amount: int description: str date: Optional[datetime] = Field(None) @@ -95,19 +95,65 @@ class SpendingBase(BaseModel): category_id: int -class SpendingCreate(SpendingBase): +class SpendingCreate(UniqueSpendingBase): pass -class SpendingPut(SpendingBase): +class Spending(UniqueSpendingBase): + id: int + owner_id: int + + +################################################ +# INSTALLMENT SPENDINGS +################################################ + + +class InstallmentSpendingBase(BaseModel): + amount: int + description: str + date: Optional[datetime] = Field(None) + group_id: int + category_id: int + amount_of_installments: int + + +class InstallmentSpendingCreate(InstallmentSpendingBase): + pass + + +class InstallmentSpending(InstallmentSpendingBase): + id: int + owner_id: int + current_installment: int + + +################################################ +# RECURRING SPENDINGS +################################################ + + +class RecurringSpendingBase(BaseModel): + amount: int + description: str + date: Optional[datetime] = Field(None) + group_id: int + category_id: int + + +class RecurringSpendingCreate(RecurringSpendingBase): pass -class Spending(SpendingBase): +class RecurringSpendingPut(RecurringSpendingBase): id: int owner_id: int +class RecurringSpending(RecurringSpendingBase): + id: int + owner_id: int + ################################################ # BUDGETS ################################################ @@ -144,6 +190,10 @@ class InviteStatus(StrEnum): ACCEPTED = auto() EXPIRED = auto() +class SpendingTypes(StrEnum): + UNIQUE = auto() + INSTALLMENT = auto() + RECURRING = auto() class InviteBase(BaseModel): creation_date: Optional[datetime] = Field(None) From d7d6bee310845f846129f8d1bdd51ba50ea8cc19 Mon Sep 17 00:00:00 2001 From: ovraulin Date: Wed, 12 Jun 2024 16:26:05 -0300 Subject: [PATCH 21/24] fix create_installment_spending --- src/crud.py | 6 +++--- src/main.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/crud.py b/src/crud.py index 70d81e7..0f4bc06 100755 --- a/src/crud.py +++ b/src/crud.py @@ -177,8 +177,8 @@ def get_unique_spendings_by_group_id(db: Session, group_id: int): ################################################ -def create_installment_spending(db: Session, spending: schemas.InstallmentSpendingCreate, user_id: int): - db_spending = models.InstallmentSpending(owner_id=user_id, **dict(spending)) +def create_installment_spending(db: Session, spending: schemas.InstallmentSpendingCreate, user_id: int, current_installment:int): + db_spending = models.InstallmentSpending(owner_id=user_id, current_installment=current_installment,**dict(spending)) db.add(db_spending) db.commit() db.refresh(db_spending) @@ -377,7 +377,7 @@ def create_payment_reminder( ################################################ -def update_balances_from_spending(db: Session, spending: models.Spending): +def update_balances_from_spending(db: Session, spending: models.UniqueSpending): group = get_group_by_id(db, spending.group_id) balances = sorted( get_balances_by_group_id(db, spending.group_id), key=lambda x: x.user_id diff --git a/src/main.py b/src/main.py index 0d41ff4..a14fd68 100755 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,7 @@ from src.mail import MailSender, mail_service, is_expired_invite from src.database import SessionLocal, engine from sqlalchemy.orm import Session +from datetime import datetime, timedelta models.Base.metadata.create_all(bind=engine) @@ -357,7 +358,17 @@ def create_installment_spending( status_code=HTTPStatus.NOT_FOUND, detail="Categoria inexistente" ) - return crud.create_installment_spending(db, spending, user.id) + res = [] + + spending_description = spending.description + amount_of_installments = spending.amount_of_installments + spending_date = spending.date + for i in range(amount_of_installments): + spending.description = f"{spending_description} | cuota {i+1}/{amount_of_installments}" + spending.date = spending_date + timedelta(days=(30*i)) + res.append(crud.create_installment_spending(db, spending, user.id, i+1)) + + return res @app.get("/group/{group_id}/installment-spending") From af7d6c09d1604a3ed16fc57f079e551a2e9e3858 Mon Sep 17 00:00:00 2001 From: ovraulin Date: Wed, 12 Jun 2024 16:42:17 -0300 Subject: [PATCH 22/24] fix some bugs --- src/crud.py | 73 ++++++++++++++++++----------------------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/src/crud.py b/src/crud.py index 0f4bc06..cd44338 100755 --- a/src/crud.py +++ b/src/crud.py @@ -182,7 +182,7 @@ def create_installment_spending(db: Session, spending: schemas.InstallmentSpendi db.add(db_spending) db.commit() db.refresh(db_spending) - # update_balances_from_spending(db, db_spending) + update_balances_from_spending(db, db_spending) db.refresh(db_spending) return db_spending @@ -196,52 +196,6 @@ def get_installment_spendings_by_group_id(db: Session, group_id: int): ) -################################################ -# PAYMENTS -################################################ - - -def create_payment(db: Session, payment: schemas.PaymentCreate): - db_payment = models.Payment(**dict(payment)) - update_balances_from_payment(db, db_payment) - db.add(db_payment) - db.commit() - db.refresh(db_payment) - return db_payment - - -def get_payments_by_group_id(db: Session, group_id: int): - return ( - db.query(models.Payment) - .filter(models.Payment.group_id == group_id) - .limit(100) - .all() - ) - - -################################################ -# PAYMENTS -################################################ - - -def create_payment(db: Session, payment: schemas.PaymentCreate): - db_payment = models.Payment(**dict(payment)) - update_balances_from_payment(db, db_payment) - db.add(db_payment) - db.commit() - db.refresh(db_payment) - return db_payment - - -def get_payments_by_group_id(db: Session, group_id: int): - return ( - db.query(models.Payment) - .filter(models.Payment.group_id == group_id) - .limit(100) - .all() - ) - - ################################################ # RECURRING SPENDINGS ################################################ @@ -252,7 +206,7 @@ def create_recurring_spending(db: Session, spending: schemas.RecurringSpendingBa db.add(db_spending) db.commit() db.refresh(db_spending) - # create_transactions_from_spending(db, db_spending) + update_balances_from_spending(db, db_spending) db.refresh(db_spending) return db_spending @@ -278,6 +232,29 @@ def put_recurring_spendings(db: Session, db_recurring_spending: models.Recurring db.refresh(db_recurring_spending) return db_recurring_spending + +################################################ +# PAYMENTS +################################################ + + +def create_payment(db: Session, payment: schemas.PaymentCreate): + db_payment = models.Payment(**dict(payment)) + update_balances_from_payment(db, db_payment) + db.add(db_payment) + db.commit() + db.refresh(db_payment) + return db_payment + + +def get_payments_by_group_id(db: Session, group_id: int): + return ( + db.query(models.Payment) + .filter(models.Payment.group_id == group_id) + .limit(100) + .all() + ) + ################################################ # BUDGETS ################################################ From a28709250244f751ef5b16b8d398b07bb243ac2f Mon Sep 17 00:00:00 2001 From: ovraulin Date: Wed, 12 Jun 2024 16:45:34 -0300 Subject: [PATCH 23/24] Remove an unused class --- src/schemas.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/schemas.py b/src/schemas.py index aa8cef7..4f83e8f 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -213,11 +213,6 @@ class InviteStatus(StrEnum): ACCEPTED = auto() EXPIRED = auto() -class SpendingTypes(StrEnum): - UNIQUE = auto() - INSTALLMENT = auto() - RECURRING = auto() - class InviteBase(BaseModel): creation_date: Optional[datetime] = Field(None) receiver_id: Optional[int] = Field(None) From 0ba4662553689fe19fea68f516fe8af45ed8f0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Wed, 12 Jun 2024 20:20:53 -0300 Subject: [PATCH 24/24] fix: update tests with new spending-related endpoints --- src/crud.py | 2 +- src/main.py | 2 +- src/schemas.py | 4 ++-- src/test_main.py | 23 +++++++++++------------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/crud.py b/src/crud.py index cd44338..a863df4 100755 --- a/src/crud.py +++ b/src/crud.py @@ -153,7 +153,7 @@ def get_all_spendings_by_group_id(db: Session, group_id: int): # UNIQUE SPENDINGS ################################################ -def create_unique_spending(db: Session, spending: schemas.SpendingCreate, user_id: int): +def create_unique_spending(db: Session, spending: schemas.UniqueSpendingCreate, user_id: int): db_spending = models.UniqueSpending(owner_id=user_id, **dict(spending)) db.add(db_spending) db.commit() diff --git a/src/main.py b/src/main.py index a14fd68..bb27bef 100755 --- a/src/main.py +++ b/src/main.py @@ -313,7 +313,7 @@ def list_group_unique_spendings(db: DbDependency, user: UserDependency, group_id @app.post("/unique-spending", status_code=HTTPStatus.CREATED) def create_unique_spending( - spending: schemas.SpendingCreate, db: DbDependency, user: UserDependency + spending: schemas.UniqueSpendingCreate, db: DbDependency, user: UserDependency ): group = crud.get_group_by_id(db, spending.group_id) diff --git a/src/schemas.py b/src/schemas.py index 4f83e8f..0882950 100755 --- a/src/schemas.py +++ b/src/schemas.py @@ -95,11 +95,11 @@ class UniqueSpendingBase(BaseModel): category_id: int -class SpendingCreate(UniqueSpendingBase): +class UniqueSpendingCreate(UniqueSpendingBase): pass -class Spending(UniqueSpendingBase): +class UniqueSpending(UniqueSpendingBase): id: int owner_id: int diff --git a/src/test_main.py b/src/test_main.py index 84336a0..7a5feb8 100755 --- a/src/test_main.py +++ b/src/test_main.py @@ -386,7 +386,7 @@ def some_spending( some_category: schemas.Category, ): response = client.post( - url="/spending", + url="/unique-spending", json={ "amount": 500, "description": "bought some féca", @@ -403,10 +403,10 @@ def some_spending( assert response_body["group_id"] == some_group.id assert response_body["category_id"] == some_category.id assert response_body - return schemas.Spending(**response_body) + return schemas.UniqueSpending(**response_body) -def test_create_new_spending(client: TestClient, some_spending: schemas.Spending): +def test_create_new_spending(client: TestClient, some_spending: schemas.UniqueSpending): # NOTE: test is inside fixture pass @@ -418,7 +418,7 @@ def test_create_new_spending_with_default_date( some_category: schemas.Category, ): response = client.post( - url="/spending", + url="/unique-spending", json={ "amount": 500, "description": "bought some féca", @@ -441,7 +441,7 @@ def test_create_new_spending_with_non_existant_category( some_group: schemas.Group, ): response = client.post( - url="/spending", + url="/unique-spending", json={ "amount": 500, "description": "bought some féca", @@ -456,16 +456,15 @@ def test_create_new_spending_with_non_existant_category( def test_get_spendings( client: TestClient, some_credentials: schemas.UserCredentials, - some_spending: schemas.Spending, + some_spending: schemas.UniqueSpending, ): response = client.get( - url="/spending", - params={"group_id": some_spending.group_id}, + url=f"/group/{some_spending.group_id}/spending", headers={"x-user": some_credentials.jwt}, ) assert response.status_code == HTTPStatus.OK assert len(response.json()) == 1 - assert schemas.Spending(**response.json()[0]) == some_spending + assert schemas.UniqueSpending(**response.json()[0]) == some_spending def test_create_spending_on_archived_group( @@ -481,7 +480,7 @@ def test_create_spending_on_archived_group( assert response.status_code == HTTPStatus.OK response = client.post( - url="/spending", + url="/unique-spending", json={ "amount": 500, "description": "bought some féca", @@ -896,7 +895,7 @@ def test_try_join_already_member( def test_balance_single_group_member( client: TestClient, some_credentials: schemas.UserCredentials, - some_spending: schemas.Spending, + some_spending: schemas.UniqueSpending, ): response = client.get( url=f"/group/{some_spending.group_id}/balance", @@ -917,7 +916,7 @@ def test_balance_single_group_member( def test_balance_multiple_members( client: TestClient, some_group_members: list[schemas.UserCredentials], - some_spending: schemas.Spending, + some_spending: schemas.UniqueSpending, ): response = client.get( url=f"/group/{some_spending.group_id}/balance",