Browse Source

Merge branch 'master' of http://gitlab.jaquin.fr/clovis/bdlg2023-sms-back

Clovis JAQUIN 2 months ago
parent
commit
d33a08a30c
44 changed files with 869 additions and 817 deletions
  1. 2 0
      Readme.md
  2. 1 3
      alembic/env.py
  3. 1 0
      alembic/versions/2023020440_init_user_model_07c71f4389b6.py
  4. 2 3
      alembic/versions/2023080523_create_first_application_load_76f559337a4a.py
  5. 3 6
      alembic/versions/2023080645_fix_nullable_send_time_596121a6a2bc.py
  6. 1 1
      alembic/versions/2023080646_remove_unique_sms_content_33a459aac32a.py
  7. 1 0
      alembic/versions/2023080647_add_nullable_sms_volunteer_4f1ef609b19a.py
  8. 4 9
      alembic/versions/2023081625_increase_slot_datamodel_5fcd397aae72.py
  9. 3 7
      alembic/versions/2023081632_fix_slot_column_name_2f90ef72e3b9.py
  10. 5 2
      alembic/versions/2024052425_add_slot_template_4ca2e4cc7b95.py
  11. 16 7
      alembic/versions/2024060200_cascade_template_deletion_to_slot_eabd3ad5aaf8.py
  12. 0 1
      alembic/versions/2024060210_stop_cascade_template_deletion_to_slot_8fee990845df.py
  13. 1 1
      alembic/versions/2024083136_update_sms_volunteer_link_466d86d3edac.py
  14. 10 1
      app/api/api.py
  15. 1 4
      app/api/deps.py
  16. 8 8
      app/api/endpoints/project.py
  17. 3 9
      app/api/endpoints/slots.py
  18. 14 3
      app/api/endpoints/sms.py
  19. 10 2
      app/api/endpoints/tags.py
  20. 1 3
      app/api/endpoints/templates.py
  21. 1 2
      app/api/endpoints/users.py
  22. 0 1
      app/api/endpoints/volunteers.py
  23. 1 1
      app/core/config.py
  24. 1 0
      app/core/session.py
  25. 3 5
      app/create_sms_batch.py
  26. 2 6
      app/importData/gsheet.py
  27. 2 1
      app/models.py
  28. 2 6
      app/restore_project.py
  29. 1 3
      app/schemas/requests.py
  30. 1 1
      app/tests/test_project.py
  31. 11 27
      app/tests/test_slot.py
  32. 1 2
      app/tests/test_sms.py
  33. 4 1
      app/tests/test_sms_batch.py
  34. 7 4
      app/tests/test_tag.py
  35. 6 18
      app/tests/test_template.py
  36. 3 9
      app/tests/test_users.py
  37. 2 2
      app/tests/test_volunteer.py
  38. 551 516
      poetry.lock
  39. 50 14
      pyproject.toml
  40. 53 63
      requirements-dev.txt
  41. 35 54
      requirements.txt
  42. 17 10
      script/KdeConnect.py
  43. 1 1
      script/send_sms.py
  44. 27 0
      update_password.py

+ 2 - 0
Readme.md

@@ -33,9 +33,11 @@ Run specific tests
 
 
 ## Update requirements
 ## Update requirements
 
 
+```
 > poetry lock
 > poetry lock
 > poetry export -f requirements.txt --output requirements.txt --without-hashes
 > poetry export -f requirements.txt --output requirements.txt --without-hashes
 > poetry export -f requirements.txt --output requirements-dev.txt --without-hashes --with dev
 > poetry export -f requirements.txt --output requirements-dev.txt --without-hashes --with dev
+```
 
 
 ## Credit
 ## Credit
 
 

+ 1 - 3
alembic/env.py

@@ -59,9 +59,7 @@ def run_migrations_offline():
 
 
 
 
 def do_run_migrations(connection):
 def do_run_migrations(connection):
-    context.configure(
-        connection=connection, target_metadata=target_metadata, compare_type=True
-    )
+    context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
 
 
     with context.begin_transaction():
     with context.begin_transaction():
         context.run_migrations()
         context.run_migrations()

+ 1 - 0
alembic/versions/2023020440_init_user_model_07c71f4389b6.py

@@ -5,6 +5,7 @@ Revises:
 Create Date: 2023-02-04 23:40:00.426237
 Create Date: 2023-02-04 23:40:00.426237
 
 
 """
 """
+
 import sqlalchemy as sa
 import sqlalchemy as sa
 
 
 from alembic import op
 from alembic import op

+ 2 - 3
alembic/versions/2023080523_create_first_application_load_76f559337a4a.py

@@ -5,6 +5,7 @@ Revises: 07c71f4389b6
 Create Date: 2023-08-05 21:23:21.563804
 Create Date: 2023-08-05 21:23:21.563804
 
 
 """
 """
+
 from alembic import op
 from alembic import op
 import sqlalchemy as sa
 import sqlalchemy as sa
 
 
@@ -75,9 +76,7 @@ def upgrade():
         sa.Column("volunteer_id", sa.UUID(as_uuid=False), nullable=False),
         sa.Column("volunteer_id", sa.UUID(as_uuid=False), nullable=False),
         sa.Column("slot_id", sa.UUID(as_uuid=False), nullable=False),
         sa.Column("slot_id", sa.UUID(as_uuid=False), nullable=False),
         sa.ForeignKeyConstraint(["slot_id"], ["slots.id"], ondelete="CASCADE"),
         sa.ForeignKeyConstraint(["slot_id"], ["slots.id"], ondelete="CASCADE"),
-        sa.ForeignKeyConstraint(
-            ["volunteer_id"], ["volunteers.id"], ondelete="CASCADE"
-        ),
+        sa.ForeignKeyConstraint(["volunteer_id"], ["volunteers.id"], ondelete="CASCADE"),
         sa.PrimaryKeyConstraint("volunteer_id", "slot_id"),
         sa.PrimaryKeyConstraint("volunteer_id", "slot_id"),
     )
     )
     op.create_table(
     op.create_table(

+ 3 - 6
alembic/versions/2023080645_fix_nullable_send_time_596121a6a2bc.py

@@ -5,6 +5,7 @@ Revises: 76f559337a4a
 Create Date: 2023-08-06 09:45:00.639402
 Create Date: 2023-08-06 09:45:00.639402
 
 
 """
 """
+
 from alembic import op
 from alembic import op
 import sqlalchemy as sa
 import sqlalchemy as sa
 from sqlalchemy.dialects import postgresql
 from sqlalchemy.dialects import postgresql
@@ -24,17 +25,13 @@ def upgrade():
         existing_type=postgresql.TIMESTAMP(timezone=True),
         existing_type=postgresql.TIMESTAMP(timezone=True),
         nullable=True,
         nullable=True,
     )
     )
-    op.alter_column(
-        "volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=True
-    )
+    op.alter_column("volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=True)
     # ### end Alembic commands ###
     # ### end Alembic commands ###
 
 
 
 
 def downgrade():
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.alter_column(
-        "volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=False
-    )
+    op.alter_column("volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=False)
     op.alter_column(
     op.alter_column(
         "sms",
         "sms",
         "send_time",
         "send_time",

+ 1 - 1
alembic/versions/2023080646_remove_unique_sms_content_33a459aac32a.py

@@ -5,8 +5,8 @@ Revises: 596121a6a2bc
 Create Date: 2023-08-06 09:46:46.191884
 Create Date: 2023-08-06 09:46:46.191884
 
 
 """
 """
+
 from alembic import op
 from alembic import op
-import sqlalchemy as sa
 
 
 
 
 # revision identifiers, used by Alembic.
 # revision identifiers, used by Alembic.

+ 1 - 0
alembic/versions/2023080647_add_nullable_sms_volunteer_4f1ef609b19a.py

@@ -5,6 +5,7 @@ Revises: 33a459aac32a
 Create Date: 2023-08-06 09:47:34.658536
 Create Date: 2023-08-06 09:47:34.658536
 
 
 """
 """
+
 from alembic import op
 from alembic import op
 import sqlalchemy as sa
 import sqlalchemy as sa
 
 

+ 4 - 9
alembic/versions/2023081625_increase_slot_datamodel_5fcd397aae72.py

@@ -5,6 +5,7 @@ Revises: 4f1ef609b19a
 Create Date: 2023-08-16 22:25:30.650590
 Create Date: 2023-08-16 22:25:30.650590
 
 
 """
 """
+
 from alembic import op
 from alembic import op
 import sqlalchemy as sa
 import sqlalchemy as sa
 
 
@@ -18,24 +19,18 @@ depends_on = None
 
 
 def upgrade():
 def upgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.add_column(
-        "slots", sa.Column("place", sa.String(), nullable=False, server_default="")
-    )
+    op.add_column("slots", sa.Column("place", sa.String(), nullable=False, server_default=""))
     op.add_column(
     op.add_column(
         "slots",
         "slots",
         sa.Column("responsibleContact", sa.String(), nullable=False, server_default=""),
         sa.Column("responsibleContact", sa.String(), nullable=False, server_default=""),
     )
     )
-    op.alter_column(
-        "volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=False
-    )
+    op.alter_column("volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=False)
     # ### end Alembic commands ###
     # ### end Alembic commands ###
 
 
 
 
 def downgrade():
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.alter_column(
-        "volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=True
-    )
+    op.alter_column("volunteers", "surname", existing_type=sa.VARCHAR(length=128), nullable=True)
     op.drop_column("slots", "responsibleContact")
     op.drop_column("slots", "responsibleContact")
     op.drop_column("slots", "place")
     op.drop_column("slots", "place")
     # ### end Alembic commands ###
     # ### end Alembic commands ###

+ 3 - 7
alembic/versions/2023081632_fix_slot_column_name_2f90ef72e3b9.py

@@ -5,8 +5,8 @@ Revises: 5fcd397aae72
 Create Date: 2023-08-16 22:32:42.841398
 Create Date: 2023-08-16 22:32:42.841398
 
 
 """
 """
+
 from alembic import op
 from alembic import op
-import sqlalchemy as sa
 
 
 
 
 # revision identifiers, used by Alembic.
 # revision identifiers, used by Alembic.
@@ -18,16 +18,12 @@ depends_on = None
 
 
 def upgrade():
 def upgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.alter_column(
-        "slots", "responsibleContact", new_column_name="responsible_contact"
-    )
+    op.alter_column("slots", "responsibleContact", new_column_name="responsible_contact")
     # ### end Alembic commands ###
     # ### end Alembic commands ###
 
 
 
 
 def downgrade():
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
 
 
-    op.alter_column(
-        "slots", "responsible_contact", new_column_name="responsibleContact"
-    )
+    op.alter_column("slots", "responsible_contact", new_column_name="responsibleContact")
     # ### end Alembic commands ###
     # ### end Alembic commands ###

+ 5 - 2
alembic/versions/2024052425_add_slot_template_4ca2e4cc7b95.py

@@ -5,6 +5,7 @@ Revises: 2f90ef72e3b9
 Create Date: 2024-05-24 09:25:42.163966
 Create Date: 2024-05-24 09:25:42.163966
 
 
 """
 """
+
 from datetime import datetime
 from datetime import datetime
 from typing import List, Optional
 from typing import List, Optional
 from alembic import op
 from alembic import op
@@ -115,7 +116,8 @@ def upgrade():
         sa.PrimaryKeyConstraint("description_id", "tag_id"),
         sa.PrimaryKeyConstraint("description_id", "tag_id"),
     )
     )
     op.add_column(
     op.add_column(
-        "slots", sa.Column("required_volunteers", sa.Integer(), nullable=False, server_default="0")
+        "slots",
+        sa.Column("required_volunteers", sa.Integer(), nullable=False, server_default="0"),
     )
     )
     op.alter_column("slots", "required_volunteers", server_default=None)
     op.alter_column("slots", "required_volunteers", server_default=None)
     op.add_column("slots", sa.Column("template_id", sa.UUID(as_uuid=False), nullable=True))
     op.add_column("slots", sa.Column("template_id", sa.UUID(as_uuid=False), nullable=True))
@@ -164,7 +166,8 @@ def downgrade():
         ),
         ),
     )
     )
     op.add_column(
     op.add_column(
-        "slots", sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=False)
+        "slots",
+        sa.Column("description", sa.VARCHAR(), autoincrement=False, nullable=False),
     )
     )
 
 
     # Revert data from template to slot
     # Revert data from template to slot

+ 16 - 7
alembic/versions/2024060200_cascade_template_deletion_to_slot_eabd3ad5aaf8.py

@@ -5,26 +5,35 @@ Revises: 4ca2e4cc7b95
 Create Date: 2024-06-02 16:00:29.689607
 Create Date: 2024-06-02 16:00:29.689607
 
 
 """
 """
+
 from alembic import op
 from alembic import op
-import sqlalchemy as sa
 
 
 
 
 # revision identifiers, used by Alembic.
 # revision identifiers, used by Alembic.
-revision = 'eabd3ad5aaf8'
-down_revision = '4ca2e4cc7b95'
+revision = "eabd3ad5aaf8"
+down_revision = "4ca2e4cc7b95"
 branch_labels = None
 branch_labels = None
 depends_on = None
 depends_on = None
 
 
 
 
 def upgrade():
 def upgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_constraint('slots_template_id_fkey', 'slots', type_='foreignkey')
-    op.create_foreign_key('slots_template_id_fkey_v2', 'slots', 'slot_templates', ['template_id'], ['id'], ondelete='CASCADE')
+    op.drop_constraint("slots_template_id_fkey", "slots", type_="foreignkey")
+    op.create_foreign_key(
+        "slots_template_id_fkey_v2",
+        "slots",
+        "slot_templates",
+        ["template_id"],
+        ["id"],
+        ondelete="CASCADE",
+    )
     # ### end Alembic commands ###
     # ### end Alembic commands ###
 
 
 
 
 def downgrade():
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
     # ### commands auto generated by Alembic - please adjust! ###
-    op.drop_constraint('slots_template_id_fkey_v2', 'slots', type_='foreignkey')
-    op.create_foreign_key('slots_template_id_fkey', 'slots', 'slot_templates', ['template_id'], ['id'])
+    op.drop_constraint("slots_template_id_fkey_v2", "slots", type_="foreignkey")
+    op.create_foreign_key(
+        "slots_template_id_fkey", "slots", "slot_templates", ["template_id"], ["id"]
+    )
     # ### end Alembic commands ###
     # ### end Alembic commands ###

+ 0 - 1
alembic/versions/2024060210_stop_cascade_template_deletion_to_slot_8fee990845df.py

@@ -7,7 +7,6 @@ Create Date: 2024-06-02 17:10:22.363720
 """
 """
 
 
 from alembic import op
 from alembic import op
-import sqlalchemy as sa
 
 
 
 
 # revision identifiers, used by Alembic.
 # revision identifiers, used by Alembic.

+ 1 - 1
alembic/versions/2024083136_update_sms_volunteer_link_466d86d3edac.py

@@ -5,8 +5,8 @@ Revises: f058df5a266c
 Create Date: 2024-08-31 14:36:54.062147
 Create Date: 2024-08-31 14:36:54.062147
 
 
 """
 """
+
 from alembic import op
 from alembic import op
-import sqlalchemy as sa
 
 
 
 
 # revision identifiers, used by Alembic.
 # revision identifiers, used by Alembic.

+ 10 - 1
app/api/api.py

@@ -1,6 +1,15 @@
 from fastapi import APIRouter
 from fastapi import APIRouter
 
 
-from app.api.endpoints import auth, project, slots, users, volunteers, sms, tags, templates
+from app.api.endpoints import (
+    auth,
+    project,
+    slots,
+    users,
+    volunteers,
+    sms,
+    tags,
+    templates,
+)
 
 
 api_router = APIRouter()
 api_router = APIRouter()
 api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
 api_router.include_router(auth.router, prefix="/auth", tags=["auth"])

+ 1 - 4
app/api/deps.py

@@ -1,5 +1,4 @@
 import time
 import time
-from collections.abc import AsyncGenerator
 from typing import Generator
 from typing import Generator
 
 
 import jwt
 import jwt
@@ -24,9 +23,7 @@ async def get_current_user(
     session: Session = Depends(get_session), token: str = Depends(reusable_oauth2)
     session: Session = Depends(get_session), token: str = Depends(reusable_oauth2)
 ) -> User:
 ) -> User:
     try:
     try:
-        payload = jwt.decode(
-            token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM]
-        )
+        payload = jwt.decode(token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM])
     except jwt.DecodeError:
     except jwt.DecodeError:
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_403_FORBIDDEN,
             status_code=status.HTTP_403_FORBIDDEN,

+ 8 - 8
app/api/endpoints/project.py

@@ -42,7 +42,7 @@ async def list_public_project(
     session: Session = Depends(deps.get_session),
     session: Session = Depends(deps.get_session),
 ):
 ):
     """Get the list of public projects"""
     """Get the list of public projects"""
-    results = session.execute(select(Project).where(Project.is_public == True))
+    results = session.execute(select(Project).where(Project.is_public))
     return results.scalars().all()
     return results.scalars().all()
 
 
 
 
@@ -57,7 +57,7 @@ async def create_project(
     session.add(project)
     session.add(project)
     try:
     try:
         session.commit()
         session.commit()
-    except IntegrityError as e:
+    except IntegrityError:
         raise HTTPException(400, "Project name already exist")
         raise HTTPException(400, "Project name already exist")
 
 
     session.refresh(project)
     session.refresh(project)
@@ -244,9 +244,9 @@ async def create_sms_batch(
             .replace("{fin}", slot.ending_time.strftime("%Hh%M"))
             .replace("{fin}", slot.ending_time.strftime("%Hh%M"))
         )
         )
         if slot.template is not None:
         if slot.template is not None:
-            slot_content = slot_content.replace(
-                "{description}", slot.template.description
-            ).replace("{respo}", slot.template.responsible_contact)
+            slot_content = slot_content.replace("{description}", slot.template.description).replace(
+                "{respo}", slot.template.responsible_contact
+            )
 
 
         sending_time = slot.starting_time - timedelta(minutes=sms_batch.delta_t)
         sending_time = slot.starting_time - timedelta(minutes=sms_batch.delta_t)
         # Skip SMS that should have been send before now
         # Skip SMS that should have been send before now
@@ -256,9 +256,9 @@ async def create_sms_batch(
             if not volunteer.automatic_sms:
             if not volunteer.automatic_sms:
                 continue
                 continue
             # Create a new SMS customized for each user attache to the slot
             # Create a new SMS customized for each user attache to the slot
-            personalized_content = slot_content.replace(
-                "{prenom}", volunteer.name
-            ).replace("{nom}", volunteer.surname)
+            personalized_content = slot_content.replace("{prenom}", volunteer.name).replace(
+                "{nom}", volunteer.surname
+            )
             sms = Sms(
             sms = Sms(
                 project_id=project_id,
                 project_id=project_id,
                 volunteer_id=volunteer.id,
                 volunteer_id=volunteer.id,

+ 3 - 9
app/api/endpoints/slots.py

@@ -55,9 +55,7 @@ async def create_slot(
     volunteers: list[UUID] = []
     volunteers: list[UUID] = []
     if input_dict["volunteers"] is not None:
     if input_dict["volunteers"] is not None:
         volunteers = input_dict["volunteers"]
         volunteers = input_dict["volunteers"]
-        await verify_id_list(
-            session, volunteers, project_id, Volunteer, "Invalid volunteer list"
-        )
+        await verify_id_list(session, volunteers, project_id, Volunteer, "Invalid volunteer list")
     del input_dict["volunteers"]
     del input_dict["volunteers"]
 
 
     slot = Slot(project_id=project_id, **input_dict)
     slot = Slot(project_id=project_id, **input_dict)
@@ -100,9 +98,7 @@ async def update_slot(
     input_dict = new_slot.model_dump(exclude_unset=True)
     input_dict = new_slot.model_dump(exclude_unset=True)
     if "volunteers" in input_dict:
     if "volunteers" in input_dict:
         volunteers: list[UUID] = input_dict["volunteers"]
         volunteers: list[UUID] = input_dict["volunteers"]
-        await verify_id_list(
-            session, volunteers, project_id, Volunteer, "Invalid volunteer list"
-        )
+        await verify_id_list(session, volunteers, project_id, Volunteer, "Invalid volunteer list")
         session.execute(
         session.execute(
             association_table_volunteer_slot.delete().where(
             association_table_volunteer_slot.delete().where(
                 association_table_volunteer_slot.c.slot_id == slot.id
                 association_table_volunteer_slot.c.slot_id == slot.id
@@ -136,7 +132,5 @@ async def delete_slot(
     session: Session = Depends(deps.get_session),
     session: Session = Depends(deps.get_session),
 ):
 ):
     """Delete a slot from the project"""
     """Delete a slot from the project"""
-    session.execute(
-        delete(Slot).where((Slot.id == slot_id) & (Slot.project_id == project_id))
-    )
+    session.execute(delete(Slot).where((Slot.id == slot_id) & (Slot.project_id == project_id)))
     session.commit()
     session.commit()

+ 14 - 3
app/api/endpoints/sms.py

@@ -1,7 +1,8 @@
+from typing import Annotated
 import datetime
 import datetime
 from uuid import UUID
 from uuid import UUID
 
 
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import delete, select
 from sqlalchemy import delete, select
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
 
 
@@ -84,10 +85,20 @@ async def delete_sms(
 async def list_sms_to_send(
 async def list_sms_to_send(
     current_user: User = Depends(deps.get_current_user),
     current_user: User = Depends(deps.get_current_user),
     session: Session = Depends(deps.get_session),
     session: Session = Depends(deps.get_session),
+    max_delay: Annotated[
+        int | None, Query(description="the maximum delay a sms should be send with")
+    ] = 10,
 ):
 ):
     """List sms that should be send by now"""
     """List sms that should be send by now"""
+
+    now = datetime.datetime.now()
+    min_sending_time = now - datetime.timedelta(minutes=max_delay)
     results = session.execute(
     results = session.execute(
-        select(Sms).where((Sms.sending_time < datetime.datetime.now()) & (Sms.send_time == None))
+        select(Sms).where(
+            (Sms.sending_time > min_sending_time)
+            & (Sms.sending_time < now)
+            & (Sms.send_time == None)  # noqa: E711
+        )
     )
     )
     return results.scalars().all()
     return results.scalars().all()
 
 
@@ -116,7 +127,7 @@ async def list_not_sent(
     session: Session = Depends(deps.get_session),
     session: Session = Depends(deps.get_session),
 ):
 ):
     """List sms that are not sent"""
     """List sms that are not sent"""
-    results = session.execute(select(Sms).where((Sms.send_time == None)))
+    results = session.execute(select(Sms).where((Sms.send_time == None)))  # noqa: E711
     return results.scalars().all()
     return results.scalars().all()
 
 
 
 

+ 10 - 2
app/api/endpoints/tags.py

@@ -50,7 +50,11 @@ async def create_tag(
 
 
     if payload.templates is not None and len(payload.templates) > 0:
     if payload.templates is not None and len(payload.templates) > 0:
         await verify_id_list(
         await verify_id_list(
-            session, payload.templates, project_id, SlotTemplate, "Invalid template list"
+            session,
+            payload.templates,
+            project_id,
+            SlotTemplate,
+            "Invalid template list",
         )
         )
         session.execute(
         session.execute(
             association_table_template_tags.insert().values(
             association_table_template_tags.insert().values(
@@ -86,7 +90,11 @@ async def update_tag(
         )
         )
         if len(payload.templates) > 0:
         if len(payload.templates) > 0:
             await verify_id_list(
             await verify_id_list(
-                session, payload.templates, project_id, SlotTemplate, "Invalid template list"
+                session,
+                payload.templates,
+                project_id,
+                SlotTemplate,
+                "Invalid template list",
             )
             )
             session.execute(
             session.execute(
                 association_table_template_tags.insert().values(
                 association_table_template_tags.insert().values(

+ 1 - 3
app/api/endpoints/templates.py

@@ -66,9 +66,7 @@ async def create_template(
     return template
     return template
 
 
 
 
-@router.post(
-    "/project/{project_id}/template/{template_id}", response_model=TemplateResponse
-)
+@router.post("/project/{project_id}/template/{template_id}", response_model=TemplateResponse)
 async def update_template(
 async def update_template(
     project_id: UUID,
     project_id: UUID,
     template_id: UUID,
     template_id: UUID,

+ 1 - 2
app/api/endpoints/users.py

@@ -11,7 +11,6 @@ from app.schemas.responses import UserResponse
 router = APIRouter()
 router = APIRouter()
 
 
 
 
-
 @router.get("", response_model=list[UserResponse])
 @router.get("", response_model=list[UserResponse])
 async def list_users(
 async def list_users(
     current_user: User = Depends(deps.get_current_user),
     current_user: User = Depends(deps.get_current_user),
@@ -39,7 +38,7 @@ async def delete_current_user(
     session.commit()
     session.commit()
 
 
 
 
-@router.post("/reset-password", response_model=UserResponse)
+@router.post("/update-password", response_model=UserResponse)
 async def reset_current_user_password(
 async def reset_current_user_password(
     user_update_password: UserUpdatePasswordRequest,
     user_update_password: UserUpdatePasswordRequest,
     session: Session = Depends(deps.get_session),
     session: Session = Depends(deps.get_session),

+ 0 - 1
app/api/endpoints/volunteers.py

@@ -3,7 +3,6 @@ from uuid import UUID
 from fastapi import APIRouter, Depends, HTTPException
 from fastapi import APIRouter, Depends, HTTPException
 from sqlalchemy import delete, select
 from sqlalchemy import delete, select
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
-from sqlalchemy.sql import func
 
 
 from app.api import deps
 from app.api import deps
 from app.api.utils import update_object_from_payload, verify_id_list
 from app.api.utils import update_object_from_payload, verify_id_list

+ 1 - 1
app/core/config.py

@@ -20,7 +20,7 @@ import tomllib
 from pathlib import Path
 from pathlib import Path
 from typing import Literal
 from typing import Literal
 
 
-from pydantic import AnyHttpUrl, EmailStr, PostgresDsn, field_validator, validator
+from pydantic import AnyHttpUrl, EmailStr
 from pydantic_settings import BaseSettings, SettingsConfigDict
 from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
 PROJECT_DIR = Path(__file__).parent.parent.parent
 PROJECT_DIR = Path(__file__).parent.parent.parent

+ 1 - 0
app/core/session.py

@@ -1,6 +1,7 @@
 """
 """
 SQLAlchemy  engine and sessions tools
 SQLAlchemy  engine and sessions tools
 """
 """
+
 from sqlalchemy import create_engine
 from sqlalchemy import create_engine
 from sqlalchemy.orm import sessionmaker
 from sqlalchemy.orm import sessionmaker
 
 

+ 3 - 5
app/create_sms_batch.py

@@ -1,12 +1,10 @@
-"""
-
-"""
+""" """
 
 
 from sqlalchemy import select
 from sqlalchemy import select
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
-from app.core import config, security
+from app.core import config
 from app.core.session import session
 from app.core.session import session
-from app.models import User, Sms, Project
+from app.models import Sms, Project
 
 
 TEST_SMS_PROJECT_NAME = "test_project pour sms"
 TEST_SMS_PROJECT_NAME = "test_project pour sms"
 NUMBER_OF_SMS = 80
 NUMBER_OF_SMS = 80

+ 2 - 6
app/importData/gsheet.py

@@ -116,9 +116,7 @@ def downloadAndSave(doc_ui: str, sheet_gid: str, file: Union[str, bytes, os.Path
         f.write(rep.content)
         f.write(rep.content)
 
 
 
 
-def getContactDataFrame(
-    csv_filename: str, skiprows: int = 2
-) -> DataFrame[ContactSchema]:
+def getContactDataFrame(csv_filename: str, skiprows: int = 2) -> DataFrame[ContactSchema]:
     df_contact = pd.read_csv(csv_filename, skiprows=skiprows)
     df_contact = pd.read_csv(csv_filename, skiprows=skiprows)
     column_to_drop = [name for name in df_contact.columns if "Unnamed" in name]
     column_to_drop = [name for name in df_contact.columns if "Unnamed" in name]
     df_contact.drop(column_to_drop, axis=1, inplace=True)
     df_contact.drop(column_to_drop, axis=1, inplace=True)
@@ -201,9 +199,7 @@ def getPlanningDataFrame(
         current_benevole_name = ""
         current_benevole_name = ""
         current_time: dict[str, datetime.datetime] = {}
         current_time: dict[str, datetime.datetime] = {}
         for j in range(skip_column, len(row)):
         for j in range(skip_column, len(row)):
-            if (current_benevole_name != "") and (
-                row[j] == "" or row[j] != current_benevole_name
-            ):
+            if (current_benevole_name != "") and (row[j] == "" or row[j] != current_benevole_name):
                 new_creneau = {
                 new_creneau = {
                     "id": str(uuid4()),
                     "id": str(uuid4()),
                     "template_id": row[0],
                     "template_id": row[0],

+ 2 - 1
app/models.py

@@ -214,7 +214,8 @@ class Sms(Base):
     project: Mapped["Project"] = relationship(back_populates="sms")
     project: Mapped["Project"] = relationship(back_populates="sms")
 
 
     volunteer_id: Mapped[str] = mapped_column(
     volunteer_id: Mapped[str] = mapped_column(
-        ForeignKey("volunteers.id", ondelete="CASCADE", onupdate="CASCADE"), nullable=True
+        ForeignKey("volunteers.id", ondelete="CASCADE", onupdate="CASCADE"),
+        nullable=True,
     )
     )
     volunteer: Mapped["Volunteer"] = relationship(back_populates="sms", cascade="all, delete")
     volunteer: Mapped["Volunteer"] = relationship(back_populates="sms", cascade="all, delete")
     created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
     created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

+ 2 - 6
app/restore_project.py

@@ -1,12 +1,8 @@
-"""
-
-"""
+""" """
 
 
 from sqlalchemy import select
 from sqlalchemy import select
-from datetime import datetime, timedelta
-from app.core import config, security
 from app.core.session import session
 from app.core.session import session
-from app.models import User, Sms, Project
+from app.models import Project
 
 
 TEST_SMS_PROJECT_NAME = "522e62c3-9620-47e1-bdd6-d856095533e7"
 TEST_SMS_PROJECT_NAME = "522e62c3-9620-47e1-bdd6-d856095533e7"
 NUMBER_OF_SMS = 80
 NUMBER_OF_SMS = 80

+ 1 - 3
app/schemas/requests.py

@@ -62,9 +62,7 @@ class ProjectSMSBatchRequest(BaseRequest):
      - {respo} slot.responsible_contact
      - {respo} slot.responsible_contact
      - {prenom} volunteer.name
      - {prenom} volunteer.name
      - {nom} volunteer.surname""",
      - {nom} volunteer.surname""",
-        examples=[
-            "Bonjour {prenom},\nTon créneau {titre} commence à {debut}.\nla com bénévole"
-        ],
+        examples=["Bonjour {prenom},\nTon créneau {titre} commence à {debut}.\nla com bénévole"],
     )
     )
     delta_t: int = Field(
     delta_t: int = Field(
         default=10,
         default=10,

+ 1 - 1
app/tests/test_project.py

@@ -96,7 +96,7 @@ async def test_create_project(client: AsyncClient, default_user_headers: dict, s
     result = session.execute(select(Project).where(Project.name == "Coucou"))
     result = session.execute(select(Project).where(Project.name == "Coucou"))
     project = result.scalars().first()
     project = result.scalars().first()
     assert project is not None
     assert project is not None
-    assert project.is_public == False
+    assert not project.is_public
 
 
     # Create a public project
     # Create a public project
     response = await client.post(
     response = await client.post(

+ 11 - 27
app/tests/test_slot.py

@@ -52,9 +52,7 @@ async def test_create_slot(
     session: Session,
     session: Session,
 ):
 ):
     # Test without autentication
     # Test without autentication
-    response = await client.post(
-        app.url_path_for("create_slot", project_id=default_project_id)
-    )
+    response = await client.post(app.url_path_for("create_slot", project_id=default_project_id))
     assert response.status_code == 401
     assert response.status_code == 401
     starting_time = datetime(1900, 1, 1)
     starting_time = datetime(1900, 1, 1)
     payload = {
     payload = {
@@ -86,9 +84,9 @@ async def test_create_slot(
     slot = [s for s in slots if s.id != default_slot_id][0]
     slot = [s for s in slots if s.id != default_slot_id][0]
     assert slot.title == "être mort"
     assert slot.title == "être mort"
     assert slot.required_volunteers == 0
     assert slot.required_volunteers == 0
-    assert abs(
-        slot.starting_time - starting_time.replace(tzinfo=timezone.utc)
-    ) < timedelta(minutes=30)
+    assert abs(slot.starting_time - starting_time.replace(tzinfo=timezone.utc)) < timedelta(
+        minutes=30
+    )
 
 
     # Test invalid payload
     # Test invalid payload
     del payload["title"]
     del payload["title"]
@@ -191,9 +189,7 @@ async def test_update_slot(
         assert response.status_code == 200
         assert response.status_code == 200
         assert response.json()["id"] == default_slot_id
         assert response.json()["id"] == default_slot_id
         if "time" in k:
         if "time" in k:
-            assert datetime.fromisoformat(response.json()[k]) == datetime.fromisoformat(
-                v
-            )
+            assert datetime.fromisoformat(response.json()[k]) == datetime.fromisoformat(v)
         else:
         else:
             assert response.json()[k] == v
             assert response.json()[k] == v
 
 
@@ -214,9 +210,7 @@ async def test_update_slot_remove_template(
         headers=default_user_headers,
         headers=default_user_headers,
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
-    slot = session.execute(
-        select(Slot).where(Slot.project_id == default_project_id)
-    ).scalar_one()
+    slot = session.execute(select(Slot).where(Slot.project_id == default_project_id)).scalar_one()
     assert slot.template_id == default_template_id
     assert slot.template_id == default_template_id
 
 
     response = await client.post(
     response = await client.post(
@@ -310,9 +304,7 @@ async def test_update_slot_volunteers(
     )
     )
 
 
     assert response.status_code == 200
     assert response.status_code == 200
-    result = session.execute(
-        select(Volunteer).where(Volunteer.id == default_volunteer_id)
-    )
+    result = session.execute(select(Volunteer).where(Volunteer.id == default_volunteer_id))
     volunteer = result.scalars().first()
     volunteer = result.scalars().first()
     assert volunteer is not None
     assert volunteer is not None
     assert volunteer.slots_id == []
     assert volunteer.slots_id == []
@@ -380,9 +372,7 @@ async def test_delete_slot_fail(
 
 
     # Cannot delete non uuid string
     # Cannot delete non uuid string
     response = await client.delete(
     response = await client.delete(
-        app.url_path_for(
-            "delete_slot", project_id=default_project_id, slot_id="not uidstr"
-        ),
+        app.url_path_for("delete_slot", project_id=default_project_id, slot_id="not uidstr"),
         headers=default_user_headers,
         headers=default_user_headers,
     )
     )
     assert response.status_code == 422
     assert response.status_code == 422
@@ -409,18 +399,14 @@ async def test_delete_slot(
     assert slot is None
     assert slot is None
 
 
     # check deletion is cascaded to volunteers
     # check deletion is cascaded to volunteers
-    result = session.execute(
-        select(Volunteer).where(Volunteer.id == default_volunteer_id)
-    )
+    result = session.execute(select(Volunteer).where(Volunteer.id == default_volunteer_id))
     volunteer: Volunteer | None = result.scalars().first()
     volunteer: Volunteer | None = result.scalars().first()
     assert volunteer is not None
     assert volunteer is not None
     assert default_slot_id not in volunteer.slots_id
     assert default_slot_id not in volunteer.slots_id
 
 
     # can delete random uuid
     # can delete random uuid
     response = await client.delete(
     response = await client.delete(
-        app.url_path_for(
-            "delete_slot", project_id=default_project_id, slot_id=uuid.uuid4()
-        ),
+        app.url_path_for("delete_slot", project_id=default_project_id, slot_id=uuid.uuid4()),
         headers=default_user_headers,
         headers=default_user_headers,
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
@@ -433,9 +419,7 @@ async def test_delete_slot_idempotent(
     default_public_project: Project,
     default_public_project: Project,
 ):
 ):
     # Idempotence test
     # Idempotence test
-    url = app.url_path_for(
-        "delete_slot", project_id=default_project_id, slot_id=default_slot_id
-    )
+    url = app.url_path_for("delete_slot", project_id=default_project_id, slot_id=default_slot_id)
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     assert response.status_code == 200
     assert response.status_code == 200

+ 1 - 2
app/tests/test_sms.py

@@ -5,7 +5,7 @@ from sqlalchemy import select
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
 
 
 from app.main import app
 from app.main import app
-from app.models import Project, Sms, Sms, Volunteer
+from app.models import Project, Sms, Volunteer
 from app.tests.conftest import default_project_id, default_volunteer_id, default_sms_id
 from app.tests.conftest import default_project_id, default_volunteer_id, default_sms_id
 
 
 
 
@@ -46,7 +46,6 @@ async def test_create_sms(
     # Test without autentication
     # Test without autentication
     response = await client.post(app.url_path_for("create_sms", project_id=default_project_id))
     response = await client.post(app.url_path_for("create_sms", project_id=default_project_id))
     assert response.status_code == 401
     assert response.status_code == 401
-    starting_time = datetime(1900, 1, 1)
     payload = {"phone_number": "06 75 75 75 75 ", "content": "sms_content"}
     payload = {"phone_number": "06 75 75 75 75 ", "content": "sms_content"}
     # test invalid project_id
     # test invalid project_id
     response = await client.post(
     response = await client.post(

+ 4 - 1
app/tests/test_sms_batch.py

@@ -225,7 +225,10 @@ starting_time = datetime.now() + timedelta(minutes=30)
         ("{prenom}", "Arthur"),
         ("{prenom}", "Arthur"),
         ("{prenom} {nom}", "Arthur Pandragon"),
         ("{prenom} {nom}", "Arthur Pandragon"),
         ("{debut}", starting_time.strftime("%Hh%M")),
         ("{debut}", starting_time.strftime("%Hh%M")),
-        ("{titre} {description}, {respo}", "replanter excalibure {description}, {respo}"),
+        (
+            "{titre} {description}, {respo}",
+            "replanter excalibure {description}, {respo}",
+        ),
     ],
     ],
 )
 )
 async def test_content_no_template(
 async def test_content_no_template(

+ 7 - 4
app/tests/test_tag.py

@@ -1,5 +1,4 @@
-from datetime import datetime, timezone
-from re import template
+from datetime import datetime
 import uuid
 import uuid
 from httpx import AsyncClient
 from httpx import AsyncClient
 import pytest
 import pytest
@@ -103,7 +102,9 @@ async def test_create_tag_with_template(
     session: Session,
     session: Session,
 ):
 ):
     template = SlotTemplate(
     template = SlotTemplate(
-        project_id=default_project_id, title="coucou", description="ceci est une description"
+        project_id=default_project_id,
+        title="coucou",
+        description="ceci est une description",
     )
     )
     session.add(template)
     session.add(template)
     session.commit()
     session.commit()
@@ -119,7 +120,9 @@ async def test_create_tag_with_template(
     assert len(template.tags) > 0
     assert len(template.tags) > 0
 
 
     template = SlotTemplate(
     template = SlotTemplate(
-        project_id=default_project_id, title="coucou", description="ceci est une description"
+        project_id=default_project_id,
+        title="coucou",
+        description="ceci est une description",
     )
     )
     session.add(template)
     session.add(template)
     session.commit()
     session.commit()

+ 6 - 18
app/tests/test_template.py

@@ -22,9 +22,7 @@ async def test_create_template_fail(
     assert response.status_code == 422
     assert response.status_code == 422
 
 
     url = app.url_path_for("create_template", project_id=uuid4())
     url = app.url_path_for("create_template", project_id=uuid4())
-    response = await client.post(
-        url, json={"title": "1st template"}, headers=default_user_headers
-    )
+    response = await client.post(url, json={"title": "1st template"}, headers=default_user_headers)
     assert response.status_code == 404
     assert response.status_code == 404
 
 
 
 
@@ -82,9 +80,7 @@ async def test_update_template_fail(
     response = await client.post(url, json=payload, headers=default_user_headers)
     response = await client.post(url, json=payload, headers=default_user_headers)
     assert response.status_code == 404
     assert response.status_code == 404
 
 
-    url = app.url_path_for(
-        "update_template", project_id=uuid4(), template_id=template_id
-    )
+    url = app.url_path_for("update_template", project_id=uuid4(), template_id=template_id)
     response = await client.post(url, json=payload, headers=default_user_headers)
     response = await client.post(url, json=payload, headers=default_user_headers)
     assert response.status_code == 404
     assert response.status_code == 404
 
 
@@ -197,16 +193,12 @@ async def test_delete_template_fail(
     response = await client.delete(url)
     response = await client.delete(url)
     assert response.status_code == 401
     assert response.status_code == 401
     # invalid tag
     # invalid tag
-    url = app.url_path_for(
-        "delete_template", project_id=project_id, template_id="default_tag_id"
-    )
+    url = app.url_path_for("delete_template", project_id=project_id, template_id="default_tag_id")
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     assert response.status_code == 422
     assert response.status_code == 422
 
 
     # invalid project_id
     # invalid project_id
-    url = app.url_path_for(
-        "delete_template", project_id="ded", template_id=default_template_id
-    )
+    url = app.url_path_for("delete_template", project_id="ded", template_id=default_template_id)
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     assert response.status_code == 422
     assert response.status_code == 422
 
 
@@ -225,16 +217,12 @@ async def test_delete_template(
     )
     )
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     assert response.status_code == 200
     assert response.status_code == 200
-    result = session.execute(
-        select(SlotTemplate).where(SlotTemplate.id == default_tag_id)
-    )
+    result = session.execute(select(SlotTemplate).where(SlotTemplate.id == default_tag_id))
     slot = result.scalars().first()
     slot = result.scalars().first()
     assert slot is None
     assert slot is None
 
 
     # can delete random uuid
     # can delete random uuid
-    url = app.url_path_for(
-        "delete_tag", project_id=default_public_project.id, tag_id=uuid4()
-    )
+    url = app.url_path_for("delete_tag", project_id=default_public_project.id, tag_id=uuid4())
     response = await client.delete(url, headers=default_user_headers)
     response = await client.delete(url, headers=default_user_headers)
     assert response.status_code == 200
     assert response.status_code == 200
 
 

+ 3 - 9
app/tests/test_users.py

@@ -12,9 +12,7 @@ from app.tests.conftest import (
 
 
 
 
 async def test_read_current_user(client: AsyncClient, default_user_headers):
 async def test_read_current_user(client: AsyncClient, default_user_headers):
-    response = await client.get(
-        app.url_path_for("read_current_user"), headers=default_user_headers
-    )
+    response = await client.get(app.url_path_for("read_current_user"), headers=default_user_headers)
     assert response.status_code == 200
     assert response.status_code == 200
     assert response.json() == {
     assert response.json() == {
         "id": default_user_id,
         "id": default_user_id,
@@ -22,9 +20,7 @@ async def test_read_current_user(client: AsyncClient, default_user_headers):
     }
     }
 
 
 
 
-async def test_delete_current_user(
-    client: AsyncClient, default_user_headers, session: Session
-):
+async def test_delete_current_user(client: AsyncClient, default_user_headers, session: Session):
     response = await client.delete(
     response = await client.delete(
         app.url_path_for("delete_current_user"), headers=default_user_headers
         app.url_path_for("delete_current_user"), headers=default_user_headers
     )
     )
@@ -49,9 +45,7 @@ async def test_reset_current_user_password(
     assert user.hashed_password != default_user_password_hash
     assert user.hashed_password != default_user_password_hash
 
 
 
 
-async def test_register_new_user(
-    client: AsyncClient, default_user_headers, session: Session
-):
+async def test_register_new_user(client: AsyncClient, default_user_headers, session: Session):
     response = await client.post(
     response = await client.post(
         app.url_path_for("register_new_user"),
         app.url_path_for("register_new_user"),
         headers=default_user_headers,
         headers=default_user_headers,

+ 2 - 2
app/tests/test_volunteer.py

@@ -1,10 +1,10 @@
 import uuid
 import uuid
 from httpx import AsyncClient
 from httpx import AsyncClient
-from sqlalchemy import false, select
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
 
 
 from app.main import app
 from app.main import app
-from app.models import Project, Slot, Sms, User, Volunteer
+from app.models import Project, Slot, Sms, Volunteer
 from app.tests.conftest import (
 from app.tests.conftest import (
     default_project_id,
     default_project_id,
     default_volunteer_id,
     default_volunteer_id,

File diff suppressed because it is too large
+ 551 - 516
poetry.lock


+ 50 - 14
pyproject.toml

@@ -2,34 +2,32 @@
 authors = ["clovis jaquin <clovis@jaquin.fr>"]
 authors = ["clovis jaquin <clovis@jaquin.fr>"]
 description = "FastAPI project that can parse gsheet planning for brass dans la garonne event and manage creating automatic SMS notification for volunteer"
 description = "FastAPI project that can parse gsheet planning for brass dans la garonne event and manage creating automatic SMS notification for volunteer"
 name = "bdlg-2023"
 name = "bdlg-2023"
-version = "0.2.2"
-package-mode = false
+version = "0.2.3"
 
 
 [tool.poetry.dependencies]
 [tool.poetry.dependencies]
-fastapi = "^0.111.0"
+fastapi = "0.116.*"
 pyjwt = {extras = ["crypto"], version = "^2.6.0"}
 pyjwt = {extras = ["crypto"], version = "^2.6.0"}
 python = "^3.11"
 python = "^3.11"
 python-multipart = "^0.0.9"
 python-multipart = "^0.0.9"
-sqlalchemy = "^2.0.1"
+sqlalchemy = "2.0.*"
 alembic = "^1.9.2"
 alembic = "^1.9.2"
 numpy = '<2.0.0'
 numpy = '<2.0.0'
 passlib = {extras = ["bcrypt"], version = "^1.7.4"}
 passlib = {extras = ["bcrypt"], version = "^1.7.4"}
-pydantic = {extras = ["dotenv", "email"], version = "^2.7.1"}
+pydantic = {extras = ["dotenv", "email"], version = "2.11.*"}
 pandas = "2.1.0"
 pandas = "2.1.0"
 pandera = "0.20.3"
 pandera = "0.20.3"
-requests = "2.31.0"
+requests = "2.32.*"
 pydantic-settings = "^2.2.1"
 pydantic-settings = "^2.2.1"
 psycopg2 = "^2.9.9"
 psycopg2 = "^2.9.9"
 
 
 [tool.poetry.group.dev.dependencies]
 [tool.poetry.group.dev.dependencies]
-autoflake = "^2.0.1"
 coverage = "^7.1.0"
 coverage = "^7.1.0"
-httpx = "^0.23.3"
+httpx = "*"
 pytest = "^7.2.1"
 pytest = "^7.2.1"
 pytest-asyncio = "^0.20.3"
 pytest-asyncio = "^0.20.3"
-uvicorn = {extras = ["standard"], version = "^0.20.0"}
+uvicorn = {extras = ["standard"], version = "*"}
 pre-commit = "^3.0.4"
 pre-commit = "^3.0.4"
-ruff = "^0.4.7"
+ruff = "*"
 
 
 [build-system]
 [build-system]
 build-backend = "poetry.core.masonry.api"
 build-backend = "poetry.core.masonry.api"
@@ -43,8 +41,46 @@ markers = ["pytest.mark.asyncio"]
 minversion = "6.0"
 minversion = "6.0"
 testpaths = ["app/tests"]
 testpaths = ["app/tests"]
 
 
-[tool.isort]
-profile = "black"
-
-[tool.black]
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+    ".bzr",
+    ".direnv",
+    ".eggs",
+    ".git",
+    ".git-rewrite",
+    ".hg",
+    ".ipynb_checkpoints",
+    ".mypy_cache",
+    ".nox",
+    ".pants.d",
+    ".pyenv",
+    ".pytest_cache",
+    ".pytype",
+    ".ruff_cache",
+    ".svn",
+    ".tox",
+    ".venv",
+    ".vscode",
+    "__pypackages__",
+    "_build",
+    "buck-out",
+    "build",
+    "dist",
+    "node_modules",
+    "site-packages",
+    "venv",
+]
 line-length = 100
 line-length = 100
+indent-width = 4
+target-version = "py311"
+
+[tool.ruff.format]
+skip-magic-trailing-comma = false
+line-ending = "auto"
+
+[tool.ruff.lint.isort]
+combine-as-imports = false
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["E402"]

+ 53 - 63
requirements-dev.txt

@@ -1,80 +1,70 @@
-alembic==1.13.2 ; python_version >= "3.11" and python_version < "4.0"
+alembic==1.16.5 ; python_version >= "3.11" and python_version < "4.0"
 annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0"
 annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0"
-anyio==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
-autoflake==2.3.1 ; python_version >= "3.11" and python_version < "4.0"
-bcrypt==4.2.0 ; python_version >= "3.11" and python_version < "4.0"
-certifi==2024.7.4 ; python_version >= "3.11" and python_version < "4.0"
-cffi==1.17.0 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy"
+anyio==4.10.0 ; python_version >= "3.11" and python_version < "4.0"
+bcrypt==4.3.0 ; python_version >= "3.11" and python_version < "4.0"
+certifi==2025.8.3 ; python_version >= "3.11" and python_version < "4.0"
+cffi==2.0.0 ; python_version >= "3.11" and platform_python_implementation != "PyPy" and python_version < "4.0"
 cfgv==3.4.0 ; python_version >= "3.11" and python_version < "4.0"
 cfgv==3.4.0 ; python_version >= "3.11" and python_version < "4.0"
-charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0"
-click==8.1.7 ; python_version >= "3.11" and python_version < "4.0"
+charset-normalizer==3.4.3 ; python_version >= "3.11" and python_version < "4.0"
+click==8.2.1 ; python_version >= "3.11" and python_version < "4.0"
 colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
 colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
-coverage==7.6.1 ; python_version >= "3.11" and python_version < "4.0"
-cryptography==43.0.0 ; python_version >= "3.11" and python_version < "4.0"
-distlib==0.3.8 ; python_version >= "3.11" and python_version < "4.0"
-dnspython==2.6.1 ; python_version >= "3.11" and python_version < "4.0"
-email-validator==2.2.0 ; python_version >= "3.11" and python_version < "4.0"
-fastapi-cli==0.0.5 ; python_version >= "3.11" and python_version < "4.0"
-fastapi==0.111.1 ; python_version >= "3.11" and python_version < "4.0"
-filelock==3.15.4 ; python_version >= "3.11" and python_version < "4.0"
-greenlet==3.0.3 ; python_version < "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.11"
-h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
-httpcore==0.16.3 ; python_version >= "3.11" and python_version < "4.0"
-httptools==0.6.1 ; python_version >= "3.11" and python_version < "4.0"
-httpx==0.23.3 ; python_version >= "3.11" and python_version < "4.0"
-identify==2.6.0 ; python_version >= "3.11" and python_version < "4.0"
-idna==3.8 ; python_version >= "3.11" and python_version < "4.0"
-iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0"
-jinja2==3.1.4 ; python_version >= "3.11" and python_version < "4.0"
-mako==1.3.5 ; python_version >= "3.11" and python_version < "4.0"
-markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0"
-markupsafe==2.1.5 ; python_version >= "3.11" and python_version < "4.0"
-mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0"
+coverage==7.10.6 ; python_version >= "3.11" and python_version < "4.0"
+cryptography==46.0.1 ; python_version >= "3.11" and python_version < "4.0"
+distlib==0.4.0 ; python_version >= "3.11" and python_version < "4.0"
+dnspython==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
+email-validator==2.3.0 ; python_version >= "3.11" and python_version < "4.0"
+fastapi==0.116.2 ; python_version >= "3.11" and python_version < "4.0"
+filelock==3.19.1 ; python_version >= "3.11" and python_version < "4.0"
+greenlet==3.2.4 ; python_version < "3.14" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.11"
+h11==0.16.0 ; python_version >= "3.11" and python_version < "4.0"
+httpcore==1.0.9 ; python_version >= "3.11" and python_version < "4.0"
+httptools==0.6.4 ; python_version >= "3.11" and python_version < "4.0"
+httpx==0.28.1 ; python_version >= "3.11" and python_version < "4.0"
+identify==2.6.14 ; python_version >= "3.11" and python_version < "4.0"
+idna==3.10 ; python_version >= "3.11" and python_version < "4.0"
+iniconfig==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
+mako==1.3.10 ; python_version >= "3.11" and python_version < "4.0"
+markupsafe==3.0.2 ; python_version >= "3.11" and python_version < "4.0"
 multimethod==1.10 ; python_version >= "3.11" and python_version < "4.0"
 multimethod==1.10 ; python_version >= "3.11" and python_version < "4.0"
-mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
+mypy-extensions==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
 nodeenv==1.9.1 ; python_version >= "3.11" and python_version < "4.0"
 nodeenv==1.9.1 ; python_version >= "3.11" and python_version < "4.0"
 numpy==1.26.4 ; python_version >= "3.11" and python_version < "4.0"
 numpy==1.26.4 ; python_version >= "3.11" and python_version < "4.0"
-packaging==24.1 ; python_version >= "3.11" and python_version < "4.0"
+packaging==25.0 ; python_version >= "3.11" and python_version < "4.0"
 pandas==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
 pandas==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
 pandera==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 pandera==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 passlib[bcrypt]==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
 passlib[bcrypt]==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
-platformdirs==4.2.2 ; python_version >= "3.11" and python_version < "4.0"
-pluggy==1.5.0 ; python_version >= "3.11" and python_version < "4.0"
+platformdirs==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
+pluggy==1.6.0 ; python_version >= "3.11" and python_version < "4.0"
 pre-commit==3.8.0 ; python_version >= "3.11" and python_version < "4.0"
 pre-commit==3.8.0 ; python_version >= "3.11" and python_version < "4.0"
-psycopg2==2.9.9 ; python_version >= "3.11" and python_version < "4.0"
-pycparser==2.22 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy"
-pydantic-core==2.20.1 ; python_version >= "3.11" and python_version < "4.0"
-pydantic-settings==2.4.0 ; python_version >= "3.11" and python_version < "4.0"
-pydantic==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
-pydantic[dotenv,email]==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
-pyflakes==3.2.0 ; python_version >= "3.11" and python_version < "4.0"
-pygments==2.18.0 ; python_version >= "3.11" and python_version < "4.0"
-pyjwt[crypto]==2.9.0 ; python_version >= "3.11" and python_version < "4.0"
+psycopg2==2.9.10 ; python_version >= "3.11" and python_version < "4.0"
+pycparser==2.23 ; python_version >= "3.11" and platform_python_implementation != "PyPy" and python_version < "4.0" and implementation_name != "PyPy"
+pydantic-core==2.33.2 ; python_version >= "3.11" and python_version < "4.0"
+pydantic-settings==2.10.1 ; python_version >= "3.11" and python_version < "4.0"
+pydantic==2.11.9 ; python_version >= "3.11" and python_version < "4.0"
+pydantic[dotenv,email]==2.11.9 ; python_version >= "3.11" and python_version < "4.0"
+pyjwt[crypto]==2.10.1 ; python_version >= "3.11" and python_version < "4.0"
 pytest-asyncio==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 pytest-asyncio==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 pytest==7.4.4 ; python_version >= "3.11" and python_version < "4.0"
 pytest==7.4.4 ; python_version >= "3.11" and python_version < "4.0"
 python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0"
 python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0"
-python-dotenv==1.0.1 ; python_version >= "3.11" and python_version < "4.0"
+python-dotenv==1.1.1 ; python_version >= "3.11" and python_version < "4.0"
 python-multipart==0.0.9 ; python_version >= "3.11" and python_version < "4.0"
 python-multipart==0.0.9 ; python_version >= "3.11" and python_version < "4.0"
-pytz==2024.1 ; python_version >= "3.11" and python_version < "4.0"
+pytz==2025.2 ; python_version >= "3.11" and python_version < "4.0"
 pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0"
 pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0"
-requests==2.31.0 ; python_version >= "3.11" and python_version < "4.0"
-rfc3986[idna2008]==1.5.0 ; python_version >= "3.11" and python_version < "4.0"
-rich==13.7.1 ; python_version >= "3.11" and python_version < "4.0"
-ruff==0.4.10 ; python_version >= "3.11" and python_version < "4.0"
-shellingham==1.5.4 ; python_version >= "3.11" and python_version < "4.0"
-six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
+requests==2.32.5 ; python_version >= "3.11" and python_version < "4.0"
+ruff==0.13.0 ; python_version >= "3.11" and python_version < "4.0"
+six==1.17.0 ; python_version >= "3.11" and python_version < "4.0"
 sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
 sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
-sqlalchemy==2.0.32 ; python_version >= "3.11" and python_version < "4.0"
-starlette==0.37.2 ; python_version >= "3.11" and python_version < "4.0"
-typeguard==4.3.0 ; python_version >= "3.11" and python_version < "4.0"
-typer==0.12.4 ; python_version >= "3.11" and python_version < "4.0"
-typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
+sqlalchemy==2.0.43 ; python_version >= "3.11" and python_version < "4.0"
+starlette==0.48.0 ; python_version >= "3.11" and python_version < "4.0"
+typeguard==4.4.4 ; python_version >= "3.11" and python_version < "4.0"
+typing-extensions==4.15.0 ; python_version >= "3.11" and python_version < "4.0"
 typing-inspect==0.9.0 ; python_version >= "3.11" and python_version < "4.0"
 typing-inspect==0.9.0 ; python_version >= "3.11" and python_version < "4.0"
-tzdata==2024.1 ; python_version >= "3.11" and python_version < "4.0"
-urllib3==2.2.2 ; python_version >= "3.11" and python_version < "4.0"
-uvicorn[standard]==0.20.0 ; python_version >= "3.11" and python_version < "4.0"
-uvloop==0.20.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.11" and python_version < "4.0"
-virtualenv==20.26.3 ; python_version >= "3.11" and python_version < "4.0"
-watchfiles==0.23.0 ; python_version >= "3.11" and python_version < "4.0"
-websockets==13.0 ; python_version >= "3.11" and python_version < "4.0"
-wrapt==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
+typing-inspection==0.4.1 ; python_version >= "3.11" and python_version < "4.0"
+tzdata==2025.2 ; python_version >= "3.11" and python_version < "4.0"
+urllib3==2.5.0 ; python_version >= "3.11" and python_version < "4.0"
+uvicorn[standard]==0.35.0 ; python_version >= "3.11" and python_version < "4.0"
+uvloop==0.21.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.11" and python_version < "4.0"
+virtualenv==20.34.0 ; python_version >= "3.11" and python_version < "4.0"
+watchfiles==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
+websockets==15.0.1 ; python_version >= "3.11" and python_version < "4.0"
+wrapt==1.17.3 ; python_version >= "3.11" and python_version < "4.0"

+ 35 - 54
requirements.txt

@@ -1,64 +1,45 @@
-alembic==1.13.2 ; python_version >= "3.11" and python_version < "4.0"
+alembic==1.16.5 ; python_version >= "3.11" and python_version < "4.0"
 annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0"
 annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0"
-anyio==4.4.0 ; python_version >= "3.11" and python_version < "4.0"
-bcrypt==4.2.0 ; python_version >= "3.11" and python_version < "4.0"
-certifi==2024.7.4 ; python_version >= "3.11" and python_version < "4.0"
-cffi==1.17.0 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy"
-charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0"
-click==8.1.7 ; python_version >= "3.11" and python_version < "4.0"
-colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
-cryptography==43.0.0 ; python_version >= "3.11" and python_version < "4.0"
-dnspython==2.6.1 ; python_version >= "3.11" and python_version < "4.0"
-email-validator==2.2.0 ; python_version >= "3.11" and python_version < "4.0"
-fastapi-cli==0.0.5 ; python_version >= "3.11" and python_version < "4.0"
-fastapi==0.111.1 ; python_version >= "3.11" and python_version < "4.0"
-greenlet==3.0.3 ; python_version < "3.13" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.11"
-h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0"
-httpcore==0.16.3 ; python_version >= "3.11" and python_version < "4.0"
-httptools==0.6.1 ; python_version >= "3.11" and python_version < "4.0"
-httpx==0.23.3 ; python_version >= "3.11" and python_version < "4.0"
-idna==3.8 ; python_version >= "3.11" and python_version < "4.0"
-jinja2==3.1.4 ; python_version >= "3.11" and python_version < "4.0"
-mako==1.3.5 ; python_version >= "3.11" and python_version < "4.0"
-markdown-it-py==3.0.0 ; python_version >= "3.11" and python_version < "4.0"
-markupsafe==2.1.5 ; python_version >= "3.11" and python_version < "4.0"
-mdurl==0.1.2 ; python_version >= "3.11" and python_version < "4.0"
+anyio==4.10.0 ; python_version >= "3.11" and python_version < "4.0"
+bcrypt==4.3.0 ; python_version >= "3.11" and python_version < "4.0"
+certifi==2025.8.3 ; python_version >= "3.11" and python_version < "4.0"
+cffi==2.0.0 ; python_version >= "3.11" and platform_python_implementation != "PyPy" and python_version < "4.0"
+charset-normalizer==3.4.3 ; python_version >= "3.11" and python_version < "4.0"
+cryptography==46.0.1 ; python_version >= "3.11" and python_version < "4.0"
+dnspython==2.8.0 ; python_version >= "3.11" and python_version < "4.0"
+email-validator==2.3.0 ; python_version >= "3.11" and python_version < "4.0"
+fastapi==0.116.2 ; python_version >= "3.11" and python_version < "4.0"
+greenlet==3.2.4 ; python_version < "3.14" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version >= "3.11"
+idna==3.10 ; python_version >= "3.11" and python_version < "4.0"
+mako==1.3.10 ; python_version >= "3.11" and python_version < "4.0"
+markupsafe==3.0.2 ; python_version >= "3.11" and python_version < "4.0"
 multimethod==1.10 ; python_version >= "3.11" and python_version < "4.0"
 multimethod==1.10 ; python_version >= "3.11" and python_version < "4.0"
-mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "4.0"
+mypy-extensions==1.1.0 ; python_version >= "3.11" and python_version < "4.0"
 numpy==1.26.4 ; python_version >= "3.11" and python_version < "4.0"
 numpy==1.26.4 ; python_version >= "3.11" and python_version < "4.0"
-packaging==24.1 ; python_version >= "3.11" and python_version < "4.0"
+packaging==25.0 ; python_version >= "3.11" and python_version < "4.0"
 pandas==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
 pandas==2.1.0 ; python_version >= "3.11" and python_version < "4.0"
 pandera==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 pandera==0.20.3 ; python_version >= "3.11" and python_version < "4.0"
 passlib[bcrypt]==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
 passlib[bcrypt]==1.7.4 ; python_version >= "3.11" and python_version < "4.0"
-psycopg2==2.9.9 ; python_version >= "3.11" and python_version < "4.0"
-pycparser==2.22 ; python_version >= "3.11" and python_version < "4.0" and platform_python_implementation != "PyPy"
-pydantic-core==2.20.1 ; python_version >= "3.11" and python_version < "4.0"
-pydantic-settings==2.4.0 ; python_version >= "3.11" and python_version < "4.0"
-pydantic==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
-pydantic[dotenv,email]==2.8.2 ; python_version >= "3.11" and python_version < "4.0"
-pygments==2.18.0 ; python_version >= "3.11" and python_version < "4.0"
-pyjwt[crypto]==2.9.0 ; python_version >= "3.11" and python_version < "4.0"
+psycopg2==2.9.10 ; python_version >= "3.11" and python_version < "4.0"
+pycparser==2.23 ; python_version >= "3.11" and platform_python_implementation != "PyPy" and python_version < "4.0" and implementation_name != "PyPy"
+pydantic-core==2.33.2 ; python_version >= "3.11" and python_version < "4.0"
+pydantic-settings==2.10.1 ; python_version >= "3.11" and python_version < "4.0"
+pydantic==2.11.9 ; python_version >= "3.11" and python_version < "4.0"
+pydantic[dotenv,email]==2.11.9 ; python_version >= "3.11" and python_version < "4.0"
+pyjwt[crypto]==2.10.1 ; python_version >= "3.11" and python_version < "4.0"
 python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0"
 python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0"
-python-dotenv==1.0.1 ; python_version >= "3.11" and python_version < "4.0"
+python-dotenv==1.1.1 ; python_version >= "3.11" and python_version < "4.0"
 python-multipart==0.0.9 ; python_version >= "3.11" and python_version < "4.0"
 python-multipart==0.0.9 ; python_version >= "3.11" and python_version < "4.0"
-pytz==2024.1 ; python_version >= "3.11" and python_version < "4.0"
-pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0"
-requests==2.31.0 ; python_version >= "3.11" and python_version < "4.0"
-rfc3986[idna2008]==1.5.0 ; python_version >= "3.11" and python_version < "4.0"
-rich==13.7.1 ; python_version >= "3.11" and python_version < "4.0"
-shellingham==1.5.4 ; python_version >= "3.11" and python_version < "4.0"
-six==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
+pytz==2025.2 ; python_version >= "3.11" and python_version < "4.0"
+requests==2.32.5 ; python_version >= "3.11" and python_version < "4.0"
+six==1.17.0 ; python_version >= "3.11" and python_version < "4.0"
 sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
 sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0"
-sqlalchemy==2.0.32 ; python_version >= "3.11" and python_version < "4.0"
-starlette==0.37.2 ; python_version >= "3.11" and python_version < "4.0"
-typeguard==4.3.0 ; python_version >= "3.11" and python_version < "4.0"
-typer==0.12.4 ; python_version >= "3.11" and python_version < "4.0"
-typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "4.0"
+sqlalchemy==2.0.43 ; python_version >= "3.11" and python_version < "4.0"
+starlette==0.48.0 ; python_version >= "3.11" and python_version < "4.0"
+typeguard==4.4.4 ; python_version >= "3.11" and python_version < "4.0"
+typing-extensions==4.15.0 ; python_version >= "3.11" and python_version < "4.0"
 typing-inspect==0.9.0 ; python_version >= "3.11" and python_version < "4.0"
 typing-inspect==0.9.0 ; python_version >= "3.11" and python_version < "4.0"
-tzdata==2024.1 ; python_version >= "3.11" and python_version < "4.0"
-urllib3==2.2.2 ; python_version >= "3.11" and python_version < "4.0"
-uvicorn[standard]==0.20.0 ; python_version >= "3.11" and python_version < "4.0"
-uvloop==0.20.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.11" and python_version < "4.0"
-watchfiles==0.23.0 ; python_version >= "3.11" and python_version < "4.0"
-websockets==13.0 ; python_version >= "3.11" and python_version < "4.0"
-wrapt==1.16.0 ; python_version >= "3.11" and python_version < "4.0"
+typing-inspection==0.4.1 ; python_version >= "3.11" and python_version < "4.0"
+tzdata==2025.2 ; python_version >= "3.11" and python_version < "4.0"
+urllib3==2.5.0 ; python_version >= "3.11" and python_version < "4.0"
+wrapt==1.17.3 ; python_version >= "3.11" and python_version < "4.0"

+ 17 - 10
script/KdeConnect.py

@@ -33,7 +33,7 @@ class KDEConnect:
         try:
         try:
             # Verify kdeconnect is available
             # Verify kdeconnect is available
             KDEConnect.run_kde_command(["-v"])
             KDEConnect.run_kde_command(["-v"])
-        except FileNotFoundError as e:
+        except FileNotFoundError:
             raise KDEConnectError("KDEconnect-cli is not available on this computer")
             raise KDEConnectError("KDEconnect-cli is not available on this computer")
 
 
         # verify the device exist
         # verify the device exist
@@ -57,14 +57,21 @@ class KDEConnect:
     def send_sms(self, phone_number: str, sms_content: str):
     def send_sms(self, phone_number: str, sms_content: str):
         # Compact phone number by removing space
         # Compact phone number by removing space
         phone = re.sub("[^+0-9]", "", phone_number)
         phone = re.sub("[^+0-9]", "", phone_number)
-        command = ["-d", self.device.id, "--send-sms", sms_content, "--destination", phone]
+        command = [
+            "-d",
+            self.device.id,
+            "--send-sms",
+            sms_content,
+            "--destination",
+            phone,
+        ]
         KDEConnect.run_kde_command(command)
         KDEConnect.run_kde_command(command)
 
 
     @staticmethod
     @staticmethod
     def run_kde_command(args: list[str]) -> str:
     def run_kde_command(args: list[str]) -> str:
         command: list[str] = ["kdeconnect-cli"]
         command: list[str] = ["kdeconnect-cli"]
         command.extend(args)
         command.extend(args)
-        process: CompletedProcess = subprocess.run(command, capture_output=True)
+        process: subprocess.CompletedProcess = subprocess.run(command, capture_output=True)
 
 
         subprocess_response = process.stdout.decode("utf-8")
         subprocess_response = process.stdout.decode("utf-8")
         logging.debug("Subprocess call : \n>> " + " ".join(command) + "\n" + subprocess_response)
         logging.debug("Subprocess call : \n>> " + " ".join(command) + "\n" + subprocess_response)
@@ -102,14 +109,14 @@ class KDEConnect:
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     kde = KDEConnect(device_name="Redmi  7A")
     kde = KDEConnect(device_name="Redmi  7A")
-    kde.send_sms("0776907462", ("160" * 53) + "\n" + "b")
-    kde.send_sms("0776907462", "162" * 54)
-    kde.send_sms("0776907462", "simple message")
-    kde.send_sms("+33776907462", "+33 number")
-    kde.send_sms("+33 77 690 746 2", "spaced number")
-    kde.send_sms("0776907462", "Multiline sms\nthis is a second line")
+    kde.send_sms("0700000000", ("160" * 53) + "\n" + "b")
+    kde.send_sms("0700000000", "162" * 54)
+    kde.send_sms("0700000000", "simple message")
+    kde.send_sms("+33700000000", "+33 number")
+    kde.send_sms("+33 77 000 000 0", "spaced number")
+    kde.send_sms("0700000000", "Multiline sms\nthis is a second line")
     kde.send_sms(
     kde.send_sms(
-        "0776907462",
+        "0700000000",
         """Lorem ipsum dolor sit amet,
         """Lorem ipsum dolor sit amet,
     consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
     consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
     Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
     Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

+ 1 - 1
script/send_sms.py

@@ -65,7 +65,7 @@ if __name__ == "__main__":
         starting_time = time.time()
         starting_time = time.time()
         try:
         try:
             main()
             main()
-        except Exception as e:
+        except Exception:
             logging.exception("An error as occured : ")
             logging.exception("An error as occured : ")
         elapsed_time = time.time() - starting_time
         elapsed_time = time.time() - starting_time
         time.sleep(max(180 - elapsed_time, 0))
         time.sleep(max(180 - elapsed_time, 0))

+ 27 - 0
update_password.py

@@ -0,0 +1,27 @@
+import argparse
+
+from sqlalchemy import select
+from app.core.security import get_password_hash
+from app.core.session import session
+from app.models import User
+
+
+def update_user_pwd(email: str, new_password: str):
+    with session() as db:
+        user = db.execute(select(User).where(User.email == email)).scalars().first()
+        if user is not None:
+            user.hashed_password = get_password_hash(new_password)
+            db.commit()
+        else:
+            raise ValueError(f"User {email} not known !")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="BDLGPlanner password manager",
+        description="This program help admin to update password from users that forgot it",
+    )
+    parser.add_argument("email")
+    parser.add_argument("password")
+    args = parser.parse_args()
+    update_user_pwd(args.email, args.password)

Some files were not shown because too many files changed in this diff