Prechádzať zdrojové kódy

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

Clovis JAQUIN 2 mesiacov pred
rodič
commit
d33a08a30c
44 zmenil súbory, kde vykonal 869 pridanie a 817 odobranie
  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
 
+```
 > poetry lock
 > poetry export -f requirements.txt --output requirements.txt --without-hashes
 > poetry export -f requirements.txt --output requirements-dev.txt --without-hashes --with dev
+```
 
 ## Credit
 

+ 1 - 3
alembic/env.py

@@ -59,9 +59,7 @@ def run_migrations_offline():
 
 
 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():
         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
 
 """
+
 import sqlalchemy as sa
 
 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
 
 """
+
 from alembic import op
 import sqlalchemy as sa
 
@@ -75,9 +76,7 @@ def upgrade():
         sa.Column("volunteer_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(
-            ["volunteer_id"], ["volunteers.id"], ondelete="CASCADE"
-        ),
+        sa.ForeignKeyConstraint(["volunteer_id"], ["volunteers.id"], ondelete="CASCADE"),
         sa.PrimaryKeyConstraint("volunteer_id", "slot_id"),
     )
     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
 
 """
+
 from alembic import op
 import sqlalchemy as sa
 from sqlalchemy.dialects import postgresql
@@ -24,17 +25,13 @@ def upgrade():
         existing_type=postgresql.TIMESTAMP(timezone=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 ###
 
 
 def downgrade():
     # ### 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(
         "sms",
         "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
 
 """
+
 from alembic import op
-import sqlalchemy as sa
 
 
 # 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
 
 """
+
 from alembic import op
 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
 
 """
+
 from alembic import op
 import sqlalchemy as sa
 
@@ -18,24 +19,18 @@ depends_on = None
 
 def upgrade():
     # ### 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(
         "slots",
         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 ###
 
 
 def downgrade():
     # ### 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", "place")
     # ### 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
 
 """
+
 from alembic import op
-import sqlalchemy as sa
 
 
 # revision identifiers, used by Alembic.
@@ -18,16 +18,12 @@ depends_on = None
 
 def upgrade():
     # ### 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 ###
 
 
 def downgrade():
     # ### 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 ###

+ 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
 
 """
+
 from datetime import datetime
 from typing import List, Optional
 from alembic import op
@@ -115,7 +116,8 @@ def upgrade():
         sa.PrimaryKeyConstraint("description_id", "tag_id"),
     )
     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.add_column("slots", sa.Column("template_id", sa.UUID(as_uuid=False), nullable=True))
@@ -164,7 +166,8 @@ def downgrade():
         ),
     )
     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

+ 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
 
 """
+
 from alembic import op
-import sqlalchemy as sa
 
 
 # revision identifiers, used by Alembic.
-revision = 'eabd3ad5aaf8'
-down_revision = '4ca2e4cc7b95'
+revision = "eabd3ad5aaf8"
+down_revision = "4ca2e4cc7b95"
 branch_labels = None
 depends_on = None
 
 
 def upgrade():
     # ### 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 ###
 
 
 def downgrade():
     # ### 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 ###

+ 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
-import sqlalchemy as sa
 
 
 # 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
 
 """
+
 from alembic import op
-import sqlalchemy as sa
 
 
 # revision identifiers, used by Alembic.

+ 10 - 1
app/api/api.py

@@ -1,6 +1,15 @@
 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.include_router(auth.router, prefix="/auth", tags=["auth"])

+ 1 - 4
app/api/deps.py

@@ -1,5 +1,4 @@
 import time
-from collections.abc import AsyncGenerator
 from typing import Generator
 
 import jwt
@@ -24,9 +23,7 @@ async def get_current_user(
     session: Session = Depends(get_session), token: str = Depends(reusable_oauth2)
 ) -> User:
     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:
         raise HTTPException(
             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),
 ):
     """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()
 
 
@@ -57,7 +57,7 @@ async def create_project(
     session.add(project)
     try:
         session.commit()
-    except IntegrityError as e:
+    except IntegrityError:
         raise HTTPException(400, "Project name already exist")
 
     session.refresh(project)
@@ -244,9 +244,9 @@ async def create_sms_batch(
             .replace("{fin}", slot.ending_time.strftime("%Hh%M"))
         )
         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)
         # Skip SMS that should have been send before now
@@ -256,9 +256,9 @@ async def create_sms_batch(
             if not volunteer.automatic_sms:
                 continue
             # 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(
                 project_id=project_id,
                 volunteer_id=volunteer.id,

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

@@ -55,9 +55,7 @@ async def create_slot(
     volunteers: list[UUID] = []
     if input_dict["volunteers"] is not None:
         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"]
 
     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)
     if "volunteers" in input_dict:
         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(
             association_table_volunteer_slot.delete().where(
                 association_table_volunteer_slot.c.slot_id == slot.id
@@ -136,7 +132,5 @@ async def delete_slot(
     session: Session = Depends(deps.get_session),
 ):
     """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()

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

@@ -1,7 +1,8 @@
+from typing import Annotated
 import datetime
 from uuid import UUID
 
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy import delete, select
 from sqlalchemy.orm import Session
 
@@ -84,10 +85,20 @@ async def delete_sms(
 async def list_sms_to_send(
     current_user: User = Depends(deps.get_current_user),
     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"""
+
+    now = datetime.datetime.now()
+    min_sending_time = now - datetime.timedelta(minutes=max_delay)
     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()
 
@@ -116,7 +127,7 @@ async def list_not_sent(
     session: Session = Depends(deps.get_session),
 ):
     """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()
 
 

+ 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:
         await verify_id_list(
-            session, payload.templates, project_id, SlotTemplate, "Invalid template list"
+            session,
+            payload.templates,
+            project_id,
+            SlotTemplate,
+            "Invalid template list",
         )
         session.execute(
             association_table_template_tags.insert().values(
@@ -86,7 +90,11 @@ async def update_tag(
         )
         if len(payload.templates) > 0:
             await verify_id_list(
-                session, payload.templates, project_id, SlotTemplate, "Invalid template list"
+                session,
+                payload.templates,
+                project_id,
+                SlotTemplate,
+                "Invalid template list",
             )
             session.execute(
                 association_table_template_tags.insert().values(

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

@@ -66,9 +66,7 @@ async def create_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(
     project_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.get("", response_model=list[UserResponse])
 async def list_users(
     current_user: User = Depends(deps.get_current_user),
@@ -39,7 +38,7 @@ async def delete_current_user(
     session.commit()
 
 
-@router.post("/reset-password", response_model=UserResponse)
+@router.post("/update-password", response_model=UserResponse)
 async def reset_current_user_password(
     user_update_password: UserUpdatePasswordRequest,
     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 sqlalchemy import delete, select
 from sqlalchemy.orm import Session
-from sqlalchemy.sql import func
 
 from app.api import deps
 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 typing import Literal
 
-from pydantic import AnyHttpUrl, EmailStr, PostgresDsn, field_validator, validator
+from pydantic import AnyHttpUrl, EmailStr
 from pydantic_settings import BaseSettings, SettingsConfigDict
 
 PROJECT_DIR = Path(__file__).parent.parent.parent

+ 1 - 0
app/core/session.py

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

+ 3 - 5
app/create_sms_batch.py

@@ -1,12 +1,10 @@
-"""
-
-"""
+""" """
 
 from sqlalchemy import select
 from datetime import datetime, timedelta
-from app.core import config, security
+from app.core import config
 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"
 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)
 
 
-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)
     column_to_drop = [name for name in df_contact.columns if "Unnamed" in name]
     df_contact.drop(column_to_drop, axis=1, inplace=True)
@@ -201,9 +199,7 @@ def getPlanningDataFrame(
         current_benevole_name = ""
         current_time: dict[str, datetime.datetime] = {}
         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 = {
                     "id": str(uuid4()),
                     "template_id": row[0],

+ 2 - 1
app/models.py

@@ -214,7 +214,8 @@ class Sms(Base):
     project: Mapped["Project"] = relationship(back_populates="sms")
 
     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")
     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 datetime import datetime, timedelta
-from app.core import config, security
 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"
 NUMBER_OF_SMS = 80

+ 1 - 3
app/schemas/requests.py

@@ -62,9 +62,7 @@ class ProjectSMSBatchRequest(BaseRequest):
      - {respo} slot.responsible_contact
      - {prenom} volunteer.name
      - {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(
         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"))
     project = result.scalars().first()
     assert project is not None
-    assert project.is_public == False
+    assert not project.is_public
 
     # Create a public project
     response = await client.post(

+ 11 - 27
app/tests/test_slot.py

@@ -52,9 +52,7 @@ async def test_create_slot(
     session: Session,
 ):
     # 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
     starting_time = datetime(1900, 1, 1)
     payload = {
@@ -86,9 +84,9 @@ async def test_create_slot(
     slot = [s for s in slots if s.id != default_slot_id][0]
     assert slot.title == "être mort"
     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
     del payload["title"]
@@ -191,9 +189,7 @@ async def test_update_slot(
         assert response.status_code == 200
         assert response.json()["id"] == default_slot_id
         if "time" in k:
-            assert datetime.fromisoformat(response.json()[k]) == datetime.fromisoformat(
-                v
-            )
+            assert datetime.fromisoformat(response.json()[k]) == datetime.fromisoformat(v)
         else:
             assert response.json()[k] == v
 
@@ -214,9 +210,7 @@ async def test_update_slot_remove_template(
         headers=default_user_headers,
     )
     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
 
     response = await client.post(
@@ -310,9 +304,7 @@ async def test_update_slot_volunteers(
     )
 
     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()
     assert volunteer is not None
     assert volunteer.slots_id == []
@@ -380,9 +372,7 @@ async def test_delete_slot_fail(
 
     # Cannot delete non uuid string
     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,
     )
     assert response.status_code == 422
@@ -409,18 +399,14 @@ async def test_delete_slot(
     assert slot is None
 
     # 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()
     assert volunteer is not None
     assert default_slot_id not in volunteer.slots_id
 
     # can delete random uuid
     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,
     )
     assert response.status_code == 200
@@ -433,9 +419,7 @@ async def test_delete_slot_idempotent(
     default_public_project: Project,
 ):
     # 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)
     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 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
 
 
@@ -46,7 +46,6 @@ async def test_create_sms(
     # Test without autentication
     response = await client.post(app.url_path_for("create_sms", project_id=default_project_id))
     assert response.status_code == 401
-    starting_time = datetime(1900, 1, 1)
     payload = {"phone_number": "06 75 75 75 75 ", "content": "sms_content"}
     # test invalid project_id
     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} {nom}", "Arthur Pandragon"),
         ("{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(

+ 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
 from httpx import AsyncClient
 import pytest
@@ -103,7 +102,9 @@ async def test_create_tag_with_template(
     session: Session,
 ):
     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.commit()
@@ -119,7 +120,9 @@ async def test_create_tag_with_template(
     assert len(template.tags) > 0
 
     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.commit()

+ 6 - 18
app/tests/test_template.py

@@ -22,9 +22,7 @@ async def test_create_template_fail(
     assert response.status_code == 422
 
     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
 
 
@@ -82,9 +80,7 @@ async def test_update_template_fail(
     response = await client.post(url, json=payload, headers=default_user_headers)
     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)
     assert response.status_code == 404
 
@@ -197,16 +193,12 @@ async def test_delete_template_fail(
     response = await client.delete(url)
     assert response.status_code == 401
     # 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)
     assert response.status_code == 422
 
     # 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)
     assert response.status_code == 422
 
@@ -225,16 +217,12 @@ async def test_delete_template(
     )
     response = await client.delete(url, headers=default_user_headers)
     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()
     assert slot is None
 
     # 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)
     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):
-    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.json() == {
         "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(
         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
 
 
-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(
         app.url_path_for("register_new_user"),
         headers=default_user_headers,

+ 2 - 2
app/tests/test_volunteer.py

@@ -1,10 +1,10 @@
 import uuid
 from httpx import AsyncClient
-from sqlalchemy import false, select
+from sqlalchemy import select
 from sqlalchemy.orm import Session
 
 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 (
     default_project_id,
     default_volunteer_id,

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 551 - 516
poetry.lock


+ 50 - 14
pyproject.toml

@@ -2,34 +2,32 @@
 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"
 name = "bdlg-2023"
-version = "0.2.2"
-package-mode = false
+version = "0.2.3"
 
 [tool.poetry.dependencies]
-fastapi = "^0.111.0"
+fastapi = "0.116.*"
 pyjwt = {extras = ["crypto"], version = "^2.6.0"}
 python = "^3.11"
 python-multipart = "^0.0.9"
-sqlalchemy = "^2.0.1"
+sqlalchemy = "2.0.*"
 alembic = "^1.9.2"
 numpy = '<2.0.0'
 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"
 pandera = "0.20.3"
-requests = "2.31.0"
+requests = "2.32.*"
 pydantic-settings = "^2.2.1"
 psycopg2 = "^2.9.9"
 
 [tool.poetry.group.dev.dependencies]
-autoflake = "^2.0.1"
 coverage = "^7.1.0"
-httpx = "^0.23.3"
+httpx = "*"
 pytest = "^7.2.1"
 pytest-asyncio = "^0.20.3"
-uvicorn = {extras = ["standard"], version = "^0.20.0"}
+uvicorn = {extras = ["standard"], version = "*"}
 pre-commit = "^3.0.4"
-ruff = "^0.4.7"
+ruff = "*"
 
 [build-system]
 build-backend = "poetry.core.masonry.api"
@@ -43,8 +41,46 @@ markers = ["pytest.mark.asyncio"]
 minversion = "6.0"
 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
+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"
-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"
-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")
-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"
-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"
 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"
 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"
-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"
-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==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-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"
-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"
-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"
-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"
-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"
-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"
-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"
-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"
 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"
-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-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"
-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"
-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"
-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:
             # Verify kdeconnect is available
             KDEConnect.run_kde_command(["-v"])
-        except FileNotFoundError as e:
+        except FileNotFoundError:
             raise KDEConnectError("KDEconnect-cli is not available on this computer")
 
         # verify the device exist
@@ -57,14 +57,21 @@ class KDEConnect:
     def send_sms(self, phone_number: str, sms_content: str):
         # Compact phone number by removing space
         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)
 
     @staticmethod
     def run_kde_command(args: list[str]) -> str:
         command: list[str] = ["kdeconnect-cli"]
         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")
         logging.debug("Subprocess call : \n>> " + " ".join(command) + "\n" + subprocess_response)
@@ -102,14 +109,14 @@ class KDEConnect:
 
 if __name__ == "__main__":
     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(
-        "0776907462",
+        "0700000000",
         """Lorem ipsum dolor sit amet,
     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.

+ 1 - 1
script/send_sms.py

@@ -65,7 +65,7 @@ if __name__ == "__main__":
         starting_time = time.time()
         try:
             main()
-        except Exception as e:
+        except Exception:
             logging.exception("An error as occured : ")
         elapsed_time = time.time() - starting_time
         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)

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov