From 34d43f4401999785dc71fd37c95d9e04154d1e9f Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:09:30 +0200 Subject: [PATCH 01/28] anyio==4.13.0 --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f09c236b9d..d97c9e79bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiofiles==24.1.0 # Asynchronous file manipulation alembic==1.13.2 # database migrations +anyio==4.13.0 arq==0.26.3 # Scheduler asyncpg==0.31.0 # PostgreSQL adapter for asynchronous operations authlib==1.6.5 @@ -34,4 +35,4 @@ SQLAlchemy[asyncio]==2.0.44 # [asyncio] allows greenlet to be installed unidecode==1.3.8 uvicorn[standard]==0.30.6 weasyprint==65.1 # HTML to PDF converter -xlsxwriter==3.2.0 +xlsxwriter==3.2.0 \ No newline at end of file From a07e75cf9f76a4cede761b6624ea1740f06842aa Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:12:44 +0200 Subject: [PATCH 02/28] Async file manipulation with anyio --- app/app.py | 8 ++--- app/core/core_endpoints/endpoints_core.py | 17 +++++---- app/core/mypayment/endpoints_mypayment.py | 4 +-- app/core/utils/config.py | 16 ++++----- app/modules/calendar/endpoints_calendar.py | 6 ++-- app/modules/raid/endpoints_raid.py | 2 +- app/modules/raid/utils/utils_raid.py | 6 ++-- app/utils/tools.py | 42 ++++++++++------------ 8 files changed, 48 insertions(+), 53 deletions(-) diff --git a/app/app.py b/app/app.py index 8837fe75d0..dd740a0859 100644 --- a/app/app.py +++ b/app/app.py @@ -4,12 +4,12 @@ import uuid from collections.abc import AsyncGenerator, Awaitable, Callable from contextlib import asynccontextmanager -from pathlib import Path from typing import TYPE_CHECKING import alembic.command as alembic_command import alembic.config as alembic_config import alembic.migration as alembic_migration +from anyio import Path from calypsso import get_calypsso_app from fastapi import FastAPI, HTTPException, Request, Response, status from fastapi.encoders import jsonable_encoder @@ -475,7 +475,7 @@ async def init_google_API( pass -def test_configuration( +async def test_configuration( settings: Settings, hyperion_error_logger: logging.Logger, ) -> None: @@ -499,8 +499,8 @@ def test_configuration( ) # Create folder for calendars if they don't already exists - Path("data/ics/").mkdir(parents=True, exist_ok=True) - Path("data/core/").mkdir(parents=True, exist_ok=True) + await Path("data/ics/").mkdir(parents=True, exist_ok=True) + await Path("data/core/").mkdir(parents=True, exist_ok=True) async def init_lifespan( diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index 65bcbaad7e..8f0bf58097 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -1,5 +1,4 @@ -from pathlib import Path - +from anyio import Path from fastapi import APIRouter, Depends, Request from fastapi.responses import FileResponse @@ -51,7 +50,7 @@ async def read_privacy(settings: Settings = Depends(get_settings)): """ return patch_identity_in_text( - Path("assets/privacy.txt").read_text(encoding="utf-8"), + await Path("assets/privacy.txt").read_text(encoding="utf-8"), settings, ) @@ -66,7 +65,7 @@ async def read_terms_and_conditions(settings: Settings = Depends(get_settings)): """ return patch_identity_in_text( - Path("assets/terms-and-conditions.txt").read_text(encoding="utf-8"), + await Path("assets/terms-and-conditions.txt").read_text(encoding="utf-8"), settings, ) @@ -80,7 +79,7 @@ async def read_mypayment_tos(settings: Settings = Depends(get_settings)): Return MyPayment latest ToS """ return patch_identity_in_text( - Path("assets/mypayment-terms-of-service.txt").read_text(encoding="utf-8"), + await Path("assets/mypayment-terms-of-service.txt").read_text(encoding="utf-8"), settings, ) @@ -95,7 +94,7 @@ async def read_support(settings: Settings = Depends(get_settings)): """ return patch_identity_in_text( - Path("assets/support.txt").read_text(encoding="utf-8"), + await Path("assets/support.txt").read_text(encoding="utf-8"), settings, ) @@ -109,7 +108,7 @@ async def read_security_txt(settings: Settings = Depends(get_settings)): Return Hyperion security.txt file """ return patch_identity_in_text( - Path("assets/security.txt").read_text(encoding="utf-8"), + await Path("assets/security.txt").read_text(encoding="utf-8"), settings, ) @@ -124,7 +123,7 @@ async def read_wellknown_security_txt(settings: Settings = Depends(get_settings) """ return patch_identity_in_text( - Path("assets/security.txt").read_text(encoding="utf-8"), + await Path("assets/security.txt").read_text(encoding="utf-8"), settings, ) @@ -139,7 +138,7 @@ async def read_robots_txt(settings: Settings = Depends(get_settings)): """ return patch_identity_in_text( - Path("assets/robots.txt").read_text(encoding="utf-8"), + await Path("assets/robots.txt").read_text(encoding="utf-8"), settings, ) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 7a38aac9b7..197dd695a3 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -4,10 +4,10 @@ import urllib.parse import uuid from datetime import UTC, datetime, timedelta -from pathlib import Path from uuid import UUID import calypsso +from anyio import Path from fastapi import ( APIRouter, BackgroundTasks, @@ -1493,7 +1493,7 @@ async def get_user_tos( accepted_tos_version=existing_user_payment.accepted_tos_version, latest_tos_version=LATEST_TOS, tos_content=patch_identity_in_text( - Path("assets/mypayment-terms-of-service.txt").read_text("utf-8"), + await Path("assets/mypayment-terms-of-service.txt").read_text("utf-8"), settings, ), max_wallet_balance=settings.MYPAYMENT_MAXIMUM_WALLET_BALANCE, diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 8694e3e58a..06d7b0a60a 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -1,10 +1,10 @@ +import tomllib from functools import cached_property -from pathlib import Path from re import Pattern from typing import Any, ClassVar import jwt -import tomllib +from anyio import Path from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key from pydantic import BaseModel, computed_field, model_validator @@ -353,16 +353,16 @@ def settings_customise_sources( @computed_field # type: ignore[prop-decorator] @cached_property - def HYPERION_VERSION(cls) -> str: - with Path("pyproject.toml").open("rb") as pyproject_binary: - pyproject = tomllib.load(pyproject_binary) + async def HYPERION_VERSION(cls) -> str: + content = await Path("pyproject.toml").read_text("utf-8") + pyproject = tomllib.loads(content) return str(pyproject["project"]["version"]) @computed_field # type: ignore[prop-decorator] @cached_property - def MINIMAL_TITAN_VERSION_CODE(cls) -> int: - with Path("pyproject.toml").open("rb") as pyproject_binary: - pyproject = tomllib.load(pyproject_binary) + async def MINIMAL_TITAN_VERSION_CODE(cls) -> int: + content = await Path("pyproject.toml").read_text("utf-8") + pyproject = tomllib.loads(content) return int(pyproject["tool"]["titan"]["minimal-titan-version-code"]) ###################################### diff --git a/app/modules/calendar/endpoints_calendar.py b/app/modules/calendar/endpoints_calendar.py index e0dee79ec2..bd9f5ba0d8 100644 --- a/app/modules/calendar/endpoints_calendar.py +++ b/app/modules/calendar/endpoints_calendar.py @@ -1,6 +1,6 @@ import uuid -from pathlib import Path +from anyio import Path from fastapi import Depends, HTTPException from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession @@ -281,10 +281,10 @@ async def recreate_ical_file( response_class=FileResponse, status_code=200, ) -async def get_icalendar_file(db: AsyncSession = Depends(get_db)): +async def get_icalendar_file(): """Get the icalendar file corresponding to the event in the database.""" - if Path(ical_file_path).exists(): + if await Path(ical_file_path).exists(): return FileResponse(ical_file_path) raise HTTPException(status_code=404) diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index b23c60f345..ec93cc7cf9 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -1,8 +1,8 @@ import logging import uuid from datetime import UTC, date, datetime -from pathlib import Path +from anyio import Path from fastapi import Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession diff --git a/app/modules/raid/utils/utils_raid.py b/app/modules/raid/utils/utils_raid.py index 6bbdd3d1ea..1703756fda 100644 --- a/app/modules/raid/utils/utils_raid.py +++ b/app/modules/raid/utils/utils_raid.py @@ -3,9 +3,9 @@ # import uuid from datetime import UTC, date, datetime -from pathlib import Path import fitz +from anyio import Path from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession @@ -220,7 +220,7 @@ async def get_all_security_files_zip( # TODO: delete the previous zip? # TODO: iotemp file? - Path("data/raid/").mkdir(parents=True, exist_ok=True) + await Path("data/raid/").mkdir(parents=True, exist_ok=True) zip_file_path = f"data/raid/Fiches_Sécurité_{datetime.now(UTC).strftime('%Y-%m-%d_%H_%M_%S')}.zip" with zipfile.ZipFile( zip_file_path, @@ -260,7 +260,7 @@ async def get_all_team_files_zip( # TODO: delete the previous zip? # TODO: iotemp file? - Path("data/raid/").mkdir(parents=True, exist_ok=True) + await Path("data/raid/").mkdir(parents=True, exist_ok=True) zip_file_path = ( f"data/raid/Teams_{datetime.now(UTC).strftime('%Y-%m-%d_%H_%M_%S')}.zip" ) diff --git a/app/utils/tools.py b/app/utils/tools.py index 0fc5ac4ca6..15815a8681 100644 --- a/app/utils/tools.py +++ b/app/utils/tools.py @@ -3,17 +3,15 @@ import os import re import secrets -import shutil import unicodedata from collections.abc import Callable, Sequence from inspect import iscoroutinefunction -from pathlib import Path from typing import TYPE_CHECKING, Any, TypeVar from uuid import UUID -import aiofiles import calypsso import fitz +from anyio import Path from fastapi import HTTPException, UploadFile from fastapi.responses import FileResponse from fastapi.templating import Jinja2Templates @@ -217,14 +215,13 @@ async def save_file_as_data( # Remove the existing file if any and create the new one # If the directory does not exist, we want to create it - Path(f"data/{directory}/").mkdir(parents=True, exist_ok=True) + await Path(f"data/{directory}/").mkdir(parents=True, exist_ok=True) try: - for filePath in Path().glob(f"data/{directory}/{filename}.*"): - filePath.unlink() + async for filePath in Path().glob(f"data/{directory}/{filename}.*"): + await filePath.unlink() - async with aiofiles.open( - f"data/{directory}/{filename}.{extension}", + async with await Path(f"data/{directory}/{filename}.{extension}").open( mode="wb", ) as buffer: # https://stackoverflow.com/questions/63580229/how-to-save-uploadfile-in-fastapi @@ -265,14 +262,13 @@ async def save_bytes_as_data( raise FileNameIsNotAnUUIDError() # If the directory does not exist, we want to create it - Path(f"data/{directory}/").mkdir(parents=True, exist_ok=True) + await Path(f"data/{directory}/").mkdir(parents=True, exist_ok=True) try: - for filePath in Path().glob(f"data/{directory}/{filename}.*"): - filePath.unlink() + async for filePath in Path().glob(f"data/{directory}/{filename}.*"): + await filePath.unlink() - async with aiofiles.open( - f"data/{directory}/{filename}.{extension}", + async with await Path(f"data/{directory}/{filename}.{extension}").open( mode="wb", ) as buffer: await buffer.write(file_bytes) @@ -284,7 +280,7 @@ async def save_bytes_as_data( raise -def get_file_path_from_data( +async def get_file_path_from_data( directory: str, filename: str | UUID, default_asset: str | None = None, @@ -307,7 +303,7 @@ def get_file_path_from_data( ) raise FileNameIsNotAnUUIDError() - for filePath in Path().glob(f"data/{directory}/{filename}.*"): + async for filePath in Path().glob(f"data/{directory}/{filename}.*"): return filePath if default_asset is not None: @@ -316,7 +312,7 @@ def get_file_path_from_data( raise FileDoesNotExistError(name=f"{directory}/{filename}.*") -def get_file_from_data( +async def get_file_from_data( directory: str, filename: str, default_asset: str | None = None, @@ -330,12 +326,12 @@ def get_file_from_data( WARNING: **NEVER** trust user input when calling this function. Always check that parameters are valid. """ - path = get_file_path_from_data(directory, filename, default_asset) + path = await get_file_path_from_data(directory, filename, default_asset) return FileResponse(path) -def delete_file_from_data( +async def delete_file_from_data( directory: str, filename: str, ): @@ -353,19 +349,19 @@ def delete_file_from_data( ) raise FileNameIsNotAnUUIDError() - for filePath in Path().glob(f"data/{directory}/{filename}.*"): - filePath.unlink() + async for filePath in Path().glob(f"data/{directory}/{filename}.*"): + await filePath.unlink() -def delete_all_folder_from_data( +async def delete_all_folder_from_data( directory: str, ) -> None: """ WARNING: this method should never be called with a directory based on user input. """ path = Path(f"data/{directory}") - if Path.exists(path): - shutil.rmtree(path) + if await path.exists(): + await path.rmdir() async def generate_pdf_from_template( From 0f8df1bc1b54bc6de318406bf27f8f45ceb50c70 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:17:49 +0200 Subject: [PATCH 03/28] Remove aiofiles --- app/core/users/endpoints_users.py | 6 +++--- app/modules/calendar/cruds_calendar.py | 6 +++--- app/modules/campaign/endpoints_campaign.py | 10 +++++----- requirements-dev.txt | 1 - requirements.txt | 1 - 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/core/users/endpoints_users.py b/app/core/users/endpoints_users.py index fde70e6258..46d0ccdbe2 100644 --- a/app/core/users/endpoints_users.py +++ b/app/core/users/endpoints_users.py @@ -3,8 +3,8 @@ import uuid from datetime import UTC, datetime, timedelta -import aiofiles import calypsso +from anyio import Path from fastapi import ( APIRouter, BackgroundTasks, @@ -788,8 +788,8 @@ async def migrate_mail_confirm( db=db, ) - async with aiofiles.open( - "data/core/mail-migration-archives.txt", + path = Path("data/core/mail-migration-archives.txt") + async with await path.open( mode="a", ) as file: await file.write( diff --git a/app/modules/calendar/cruds_calendar.py b/app/modules/calendar/cruds_calendar.py index e679fb19e7..8e8d4a54d8 100644 --- a/app/modules/calendar/cruds_calendar.py +++ b/app/modules/calendar/cruds_calendar.py @@ -1,7 +1,7 @@ from collections.abc import Sequence from datetime import UTC, date, datetime, timedelta -import aiofiles +from anyio import Path from icalendar import Calendar, Event, vRecur from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -10,7 +10,7 @@ from app.modules.calendar import models_calendar, schemas_calendar from app.modules.calendar.types_calendar import Decision -calendar_file_path = "data/ics/ae_calendar.ics" +calendar_file_path = Path("data/ics/ae_calendar.ics") async def get_all_events(db: AsyncSession) -> Sequence[models_calendar.Event]: @@ -140,5 +140,5 @@ async def create_icalendar_file(db: AsyncSession) -> None: calendar.add_component(ical_event) - async with aiofiles.open(calendar_file_path, mode="wb") as calendar_file: + async with await calendar_file_path.open(mode="wb") as calendar_file: await calendar_file.write(calendar.to_ical()) diff --git a/app/modules/campaign/endpoints_campaign.py b/app/modules/campaign/endpoints_campaign.py index 9ccc370a58..851f8f3438 100644 --- a/app/modules/campaign/endpoints_campaign.py +++ b/app/modules/campaign/endpoints_campaign.py @@ -3,7 +3,7 @@ import uuid from datetime import UTC, datetime -import aiofiles +from anyio import Path from fastapi import Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession @@ -493,8 +493,8 @@ async def open_vote( # Archive all changes to a json file lists = await cruds_campaign.get_lists(db=db) - async with aiofiles.open( - f"data/campaigns/lists-{datetime.now(tz=UTC).date().isoformat()}.json", + path = Path(f"data/campaigns/lists-{datetime.now(UTC).date().isoformat()}.json") + async with await path.open( mode="w", ) as file: await file.write(json.dumps([liste.as_dict() for liste in lists])) @@ -610,8 +610,8 @@ async def reset_vote( # Archive results to a json file results = await get_results(db=db, user=user) - async with aiofiles.open( - f"data/campaigns/results-{datetime.now(UTC).date().isoformat()}.json", + path = Path(f"data/campaigns/results-{datetime.now(UTC).date().isoformat()}.json") + async with await path.open( mode="w", ) as file: await file.write( diff --git a/requirements-dev.txt b/requirements-dev.txt index 083d0c399c..6760e1481b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,6 @@ pytest-cov==6.1.1 pytest-mock==3.14.1 pytest==9.0.1 ruff==0.11.8 -types-aiofiles==24.1.0.20250516 types-Authlib==1.5.0.20250516 types-fpdf2==2.8.3.20250516 types-psutil==7.0.0.20250601 diff --git a/requirements.txt b/requirements.txt index d97c9e79bf..1d798bde91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -aiofiles==24.1.0 # Asynchronous file manipulation alembic==1.13.2 # database migrations anyio==4.13.0 arq==0.26.3 # Scheduler From 668294bd26682e5865d43406f67328ab996e3ee5 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:22:35 +0200 Subject: [PATCH 04/28] Format --- app/modules/booking/endpoints_booking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index 5db723bfba..e7e07808d5 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -1,10 +1,10 @@ import logging import uuid from datetime import UTC, datetime +from zoneinfo import ZoneInfo from fastapi import Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from zoneinfo import ZoneInfo from app.core.groups import cruds_groups from app.core.groups.groups_type import AccountType From 2bc125232b786ab6a285f902198f2bf8e4a78308 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:28:22 +0200 Subject: [PATCH 05/28] Don't use async methods in computed properties --- app/core/utils/config.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 06d7b0a60a..8c95fb6aa6 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -1,10 +1,10 @@ +import pathlib import tomllib from functools import cached_property from re import Pattern from typing import Any, ClassVar import jwt -from anyio import Path from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key from pydantic import BaseModel, computed_field, model_validator @@ -353,16 +353,16 @@ def settings_customise_sources( @computed_field # type: ignore[prop-decorator] @cached_property - async def HYPERION_VERSION(cls) -> str: - content = await Path("pyproject.toml").read_text("utf-8") - pyproject = tomllib.loads(content) + def HYPERION_VERSION(cls) -> str: + with pathlib.Path("pyproject.toml").open("rb") as pyproject_binary: + pyproject = tomllib.load(pyproject_binary) return str(pyproject["project"]["version"]) @computed_field # type: ignore[prop-decorator] @cached_property - async def MINIMAL_TITAN_VERSION_CODE(cls) -> int: - content = await Path("pyproject.toml").read_text("utf-8") - pyproject = tomllib.loads(content) + def MINIMAL_TITAN_VERSION_CODE(cls) -> int: + with pathlib.Path("pyproject.toml").open("rb") as pyproject_binary: + pyproject = tomllib.load(pyproject_binary) return int(pyproject["tool"]["titan"]["minimal-titan-version-code"]) ###################################### From 57cae5490f2cb24812dddae785175bb172b474eb Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:33:51 +0200 Subject: [PATCH 06/28] Fix --- app/core/mypayment/endpoints_mypayment.py | 2 +- app/core/users/endpoints_users.py | 4 +- app/modules/advert/endpoints_advert.py | 2 +- app/modules/campaign/endpoints_campaign.py | 2 +- app/modules/cinema/endpoints_cinema.py | 2 +- app/modules/ph/endpoints_ph.py | 8 ++-- app/modules/phonebook/endpoints_phonebook.py | 2 +- app/modules/raffle/endpoints_raffle.py | 4 +- app/modules/raid/endpoints_raid.py | 6 +-- .../endpoints_recommendation.py | 2 +- .../endpoints_sport_competition.py | 8 ++-- tests/core/test_utils.py | 43 ++++++++++--------- 12 files changed, 44 insertions(+), 41 deletions(-) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index 197dd695a3..bac104f0d5 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -2944,7 +2944,7 @@ async def download_invoice( status_code=403, detail="User is not allowed to access this invoice", ) - return get_file_from_data( + return await get_file_from_data( directory="mypayment/invoices", filename=str(invoice_id), ) diff --git a/app/core/users/endpoints_users.py b/app/core/users/endpoints_users.py index 46d0ccdbe2..dae2a3b104 100644 --- a/app/core/users/endpoints_users.py +++ b/app/core/users/endpoints_users.py @@ -1034,7 +1034,7 @@ async def read_own_profile_picture( Get the profile picture of the authenticated user. """ - return get_file_from_data( + return await get_file_from_data( directory="profile-pictures", filename=str(user.id), default_asset="assets/images/default_profile_picture.png", @@ -1060,7 +1060,7 @@ async def read_user_profile_picture( if not db_user: raise HTTPException(status_code=404, detail="User not found") - return get_file_from_data( + return await get_file_from_data( directory="profile-pictures", filename=str(user_id), default_asset="assets/images/default_profile_picture.png", diff --git a/app/modules/advert/endpoints_advert.py b/app/modules/advert/endpoints_advert.py index 0eae5eacfc..883072db79 100644 --- a/app/modules/advert/endpoints_advert.py +++ b/app/modules/advert/endpoints_advert.py @@ -428,7 +428,7 @@ async def read_advert_image( detail="The advert does not exist", ) - return get_file_from_data( + return await get_file_from_data( default_asset="assets/images/default_advert.png", directory="adverts", filename=str(advert_id), diff --git a/app/modules/campaign/endpoints_campaign.py b/app/modules/campaign/endpoints_campaign.py index 851f8f3438..c0386bf976 100644 --- a/app/modules/campaign/endpoints_campaign.py +++ b/app/modules/campaign/endpoints_campaign.py @@ -892,7 +892,7 @@ async def read_campaigns_logo( detail="The list does not exist.", ) - return get_file_from_data( + return await get_file_from_data( directory="campaigns", filename=str(list_id), default_asset="assets/images/default_campaigns_logo.png", diff --git a/app/modules/cinema/endpoints_cinema.py b/app/modules/cinema/endpoints_cinema.py index aabae310be..f7e9365d1c 100644 --- a/app/modules/cinema/endpoints_cinema.py +++ b/app/modules/cinema/endpoints_cinema.py @@ -268,7 +268,7 @@ async def read_session_poster( detail="The session does not exist.", ) - return get_file_from_data( + return await get_file_from_data( default_asset="assets/images/default_movie.png", directory="cinemasessions", filename=str(session_id), diff --git a/app/modules/ph/endpoints_ph.py b/app/modules/ph/endpoints_ph.py index a7590b8ebe..12f0c4da02 100644 --- a/app/modules/ph/endpoints_ph.py +++ b/app/modules/ph/endpoints_ph.py @@ -73,7 +73,7 @@ async def get_paper_pdf( detail="The paper does not exist.", ) - return get_file_from_data( + return await get_file_from_data( default_asset="assets/pdf/default_ph.pdf", directory="ph/pdf", filename=str(paper_id), @@ -228,7 +228,7 @@ async def get_cover( detail="The paper does not exist.", ) - return get_file_from_data( + return await get_file_from_data( default_asset="assets/images/default_cover.jpeg", directory="ph/cover", filename=str(paper_id), @@ -279,12 +279,12 @@ async def delete_paper( detail="Invalid paper_id", ) - delete_file_from_data( + await delete_file_from_data( directory="ph/pdf", filename=str(paper_id), ) - delete_file_from_data( + await delete_file_from_data( directory="ph/cover", filename=str(paper_id), ) diff --git a/app/modules/phonebook/endpoints_phonebook.py b/app/modules/phonebook/endpoints_phonebook.py index 5643d60192..227263ad95 100644 --- a/app/modules/phonebook/endpoints_phonebook.py +++ b/app/modules/phonebook/endpoints_phonebook.py @@ -835,7 +835,7 @@ async def read_association_logo( if association is None: raise HTTPException(404, "The Association does not exist.") - return get_file_from_data( + return await get_file_from_data( directory="associations", filename=association_id, default_asset="assets/images/default_association_picture.png", diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 0e087158c0..75ea4573e7 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -285,7 +285,7 @@ async def read_raffle_logo( if not raffle: raise HTTPException(status_code=404, detail="Raffle not found") - return get_file_from_data( + return await get_file_from_data( directory="raffle-pictures", filename=str(raffle_id), default_asset="assets/images/default_raffle_logo.png", @@ -843,7 +843,7 @@ async def read_prize_logo( if not prize: raise HTTPException(status_code=404, detail="Prize not found") - return get_file_from_data( + return await get_file_from_data( directory="raffle-prize_picture", filename=str(prize_id), default_asset="assets/images/default_prize_picture.png", diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index ec93cc7cf9..f223877bb8 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -391,7 +391,7 @@ async def delete_all_teams( # Delete all participants from the database await cruds_raid.delete_all_participant(db) - delete_all_folder_from_data("raid") + await delete_all_folder_from_data("raid") @module.router.post( @@ -477,7 +477,7 @@ async def read_document( # The document can be a global document information = await get_core_data(coredata_raid.RaidInformation, db) if document_id in {information.raid_rules_id, information.raid_information_id}: - return get_file_from_data( + return await get_file_from_data( default_asset="assets/pdf/default_PDF.pdf", directory="raid", filename=str(document_id), @@ -501,7 +501,7 @@ async def read_document( detail="The owner of this document is not a member of your team.", ) - return get_file_from_data( + return await get_file_from_data( default_asset="assets/images/default_advert.png", # TODO: get a default document directory="raid", filename=str(document_id), diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index f0ad8492b7..cf065df9bf 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -169,7 +169,7 @@ async def read_recommendation_image( if not recommendation: raise HTTPException(status_code=404, detail="The recommendation does not exist") - return get_file_from_data( + return await get_file_from_data( default_asset="assets/images/default_recommendation.png", directory="recommendations", filename=str(recommendation_id), diff --git a/app/modules/sport_competition/endpoints_sport_competition.py b/app/modules/sport_competition/endpoints_sport_competition.py index 70e7db5ea9..72b5f0be0f 100644 --- a/app/modules/sport_competition/endpoints_sport_competition.py +++ b/app/modules/sport_competition/endpoints_sport_competition.py @@ -2256,7 +2256,7 @@ async def download_participant_certificate( status_code=404, detail="No certificate uploaded for this participant", ) - return get_file_from_data( + return await get_file_from_data( directory="sport_competition/certificates", filename=str(participant.certificate_file_id), ) @@ -2669,7 +2669,7 @@ async def withdraw_from_sport( db, ) if participant.certificate_file_id is not None: - delete_file_from_data( + await delete_file_from_data( directory="sport_competition/certificates", filename=str(participant.certificate_file_id), ) @@ -2749,7 +2749,7 @@ async def delete_participant( db, ) if participant.certificate_file_id is not None: - delete_file_from_data( + await delete_file_from_data( directory="sport_competition/certificates", filename=str(participant.certificate_file_id), ) @@ -2794,7 +2794,7 @@ async def delete_participant_certificate_file( None, db, ) - delete_file_from_data( + await delete_file_from_data( directory="sport_competition/certificates", filename=str(participant.certificate_file_id), ) diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 33a07c02b9..600031e17e 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -1,9 +1,10 @@ +import pathlib import shutil import uuid -from pathlib import Path import pytest import pytest_asyncio +from anyio import Path from fastapi import HTTPException, UploadFile from starlette.datastructures import Headers @@ -68,7 +69,7 @@ async def init_objects() -> None: async def test_save_file() -> None: valid_uuid = str(uuid.uuid4()) - with Path("assets/images/default_profile_picture.png").open("rb") as file: + with pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file: await save_file_as_data( upload_file=UploadFile( file, @@ -83,7 +84,7 @@ async def test_save_file_with_invalid_content_type() -> None: valid_uuid = str(uuid.uuid4()) with ( pytest.raises(HTTPException, match="400: Invalid file format, supported*"), - Path("assets/images/default_profile_picture.png").open("rb") as file, + pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file, ): await save_file_as_data( upload_file=UploadFile( @@ -102,7 +103,7 @@ async def test_save_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: FileNameIsNotAnUUIDError, match="The filename is not a valid UUID", ), - Path("assets/images/default_profile_picture.png").open("rb") as file, + pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file, ): await save_file_as_data( upload_file=UploadFile(file), @@ -113,7 +114,7 @@ async def test_save_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: async def test_save_bytes() -> None: valid_uuid = str(uuid.uuid4()) - with Path("assets/images/default_profile_picture.png").open("rb") as file: + with pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file: await save_bytes_as_data( file_bytes=file.read(), directory="test", @@ -129,7 +130,7 @@ async def test_save_bytes_raise_a_value_error_if_filename_isnt_an_uuid() -> None FileNameIsNotAnUUIDError, match="The filename is not a valid UUID", ), - Path("assets/images/default_profile_picture.png").open("rb") as file, + pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file, ): await save_bytes_as_data( file_bytes=file.read(), @@ -139,7 +140,7 @@ async def test_save_bytes_raise_a_value_error_if_filename_isnt_an_uuid() -> None ) -def test_get_existing_file_path_with_valid_uuid() -> None: +async def test_get_existing_file_path_with_valid_uuid() -> None: valid_uuid = str(uuid.uuid4()) default_asset = "assets/images/default_profile_picture.png" file_path = Path(f"data/test/{valid_uuid}.png") @@ -147,7 +148,7 @@ def test_get_existing_file_path_with_valid_uuid() -> None: default_asset, file_path, ) - returned_path = get_file_path_from_data( + returned_path = await get_file_path_from_data( directory="test", filename=valid_uuid, default_asset=default_asset, @@ -155,9 +156,11 @@ def test_get_existing_file_path_with_valid_uuid() -> None: assert returned_path == file_path -def test_get_non_existing_file_path_with_valid_uuid_return_default_asset() -> None: +async def test_get_non_existing_file_path_with_valid_uuid_return_default_asset() -> ( + None +): valid_uuid = str(uuid.uuid4()) - path = get_file_path_from_data( + path = await get_file_path_from_data( directory="test", filename=valid_uuid, default_asset="assets/images/default_profile_picture.png", @@ -165,23 +168,23 @@ def test_get_non_existing_file_path_with_valid_uuid_return_default_asset() -> No assert path == Path("assets/images/default_profile_picture.png") -def test_get_file_path_raise_a_value_error_if_filename_isnt_an_uuid() -> None: +async def test_get_file_path_raise_a_value_error_if_filename_isnt_an_uuid() -> None: not_a_uuid = "not_a_uuid" with pytest.raises( FileNameIsNotAnUUIDError, match="The filename is not a valid UUID", ): - get_file_path_from_data( + await get_file_path_from_data( directory="test", filename=not_a_uuid, default_asset="default_asset", ) -def test_get_file_with_valid_uuid() -> None: +async def test_get_file_with_valid_uuid() -> None: valid_uuid = str(uuid.uuid4()) default_asset = "assets/images/default_profile_picture.png" - file = get_file_from_data( + file = await get_file_from_data( directory="test", filename=valid_uuid, default_asset=default_asset, @@ -189,20 +192,20 @@ def test_get_file_with_valid_uuid() -> None: assert file.path == Path(default_asset) -def test_get_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: +async def test_get_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: not_a_uuid = "not_a_uuid" with pytest.raises( FileNameIsNotAnUUIDError, match="The filename is not a valid UUID", ): - get_file_from_data( + await get_file_from_data( directory="test", filename=not_a_uuid, default_asset="default_asset", ) -def test_delete_file_with_valid_uuid() -> None: +async def test_delete_file_with_valid_uuid() -> None: valid_uuid = str(uuid.uuid4()) default_asset = "assets/images/default_profile_picture.png" file_png_path = Path(f"data/test/{valid_uuid}.png") @@ -217,7 +220,7 @@ def test_delete_file_with_valid_uuid() -> None: file_jpg_path, ) - delete_file_from_data( + await delete_file_from_data( directory="test", filename=valid_uuid, ) @@ -225,13 +228,13 @@ def test_delete_file_with_valid_uuid() -> None: assert not Path(file_jpg_path).is_file() -def test_delete_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: +async def test_delete_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: not_a_uuid = "not_a_uuid" with pytest.raises( FileNameIsNotAnUUIDError, match="The filename is not a valid UUID", ): - delete_file_from_data( + await delete_file_from_data( directory="test", filename=not_a_uuid, ) From bc08d8b06e5171b7e10724563e810bb44f720668 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:34:09 +0200 Subject: [PATCH 07/28] Format --- app/core/utils/config.py | 2 +- app/modules/booking/endpoints_booking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 8c95fb6aa6..7e7759e504 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -1,10 +1,10 @@ import pathlib -import tomllib from functools import cached_property from re import Pattern from typing import Any, ClassVar import jwt +import tomllib from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key from pydantic import BaseModel, computed_field, model_validator diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index e7e07808d5..5db723bfba 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -1,10 +1,10 @@ import logging import uuid from datetime import UTC, datetime -from zoneinfo import ZoneInfo from fastapi import Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession +from zoneinfo import ZoneInfo from app.core.groups import cruds_groups from app.core.groups.groups_type import AccountType From 7a20a2320a141a38a781031f48e1a5f3cf23a946 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:50:48 +0200 Subject: [PATCH 08/28] Fix tests --- app/modules/raid/utils/utils_raid.py | 4 ++-- app/utils/tools.py | 9 +++++---- tests/core/test_utils.py | 6 +++--- tests/modules/test_raid.py | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/modules/raid/utils/utils_raid.py b/app/modules/raid/utils/utils_raid.py index 1703756fda..8d0d89cbf7 100644 --- a/app/modules/raid/utils/utils_raid.py +++ b/app/modules/raid/utils/utils_raid.py @@ -233,7 +233,7 @@ async def get_all_security_files_zip( information, team.number, ) - src_pdf = get_file_path_from_data( + src_pdf = await get_file_path_from_data( directory="raid/security_file", filename=file_id, ) @@ -272,7 +272,7 @@ async def get_all_team_files_zip( file_id = await generate_recap_file_pdf( team, ) - src_pdf = get_file_path_from_data( + src_pdf = await get_file_path_from_data( directory="raid/recap", filename=file_id, ) diff --git a/app/utils/tools.py b/app/utils/tools.py index 15815a8681..9b9e73b55f 100644 --- a/app/utils/tools.py +++ b/app/utils/tools.py @@ -3,6 +3,7 @@ import os import re import secrets +import shutil import unicodedata from collections.abc import Callable, Sequence from inspect import iscoroutinefunction @@ -361,7 +362,7 @@ async def delete_all_folder_from_data( """ path = Path(f"data/{directory}") if await path.exists(): - await path.rmdir() + shutil.rmtree(path) async def generate_pdf_from_template( @@ -404,7 +405,7 @@ async def generate_pdf_from_template( ) -def concat_pdf( +async def concat_pdf( source_directory: str, source_filename: str | UUID, output_pdf: fitz.Document, @@ -413,7 +414,7 @@ def concat_pdf( Add the content of the PDF file located in data to the `output_pdf` document. """ - source_file_path = get_file_path_from_data( + source_file_path = await get_file_path_from_data( directory=source_directory, filename=source_filename, ) @@ -435,7 +436,7 @@ async def save_pdf_first_page_as_image( WARNING: **NEVER** trust user input when calling this function. Always check that parameters are valid. """ - pdf_file_path = get_file_path_from_data( + pdf_file_path = await get_file_path_from_data( input_pdf_directory, filename, default_pdf_path, diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 600031e17e..54cbd09b52 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -224,8 +224,8 @@ async def test_delete_file_with_valid_uuid() -> None: directory="test", filename=valid_uuid, ) - assert not Path(file_png_path).is_file() - assert not Path(file_jpg_path).is_file() + assert not await Path(file_png_path).is_file() + assert not await Path(file_jpg_path).is_file() async def test_delete_file_raise_a_value_error_if_filename_isnt_an_uuid() -> None: @@ -249,7 +249,7 @@ async def test_save_pdf_first_page_as_image() -> None: filename=valid_uuid, default_pdf_path="assets/pdf/default_PDF.pdf", ) - assert Path(f"data/test/image/{valid_uuid}.jpg").is_file() + assert await Path(f"data/test/image/{valid_uuid}.jpg").is_file() async def test_get_core_data() -> None: diff --git a/tests/modules/test_raid.py b/tests/modules/test_raid.py index 72c5afc70b..f458a25cc8 100644 --- a/tests/modules/test_raid.py +++ b/tests/modules/test_raid.py @@ -1,11 +1,11 @@ import datetime import shutil import uuid -from pathlib import Path from unittest.mock import Mock import pytest import pytest_asyncio +from anyio import Path from fastapi.testclient import TestClient from pytest_mock import MockerFixture @@ -204,7 +204,7 @@ async def init_objects() -> None: await add_object_to_db(validated_team) - Path("data/raid/").mkdir(parents=True, exist_ok=True) + await Path("data/raid/").mkdir(parents=True, exist_ok=True) default_asset = "assets/pdf/default_PDF.pdf" expected_files = [ "-1_ValidatedTeam_Captain_Validated.pdf", From b32e7d672558c0fa5e5ec227a3c039a006bb7df7 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:20:10 +0200 Subject: [PATCH 09/28] Ruff 0.15.10 # Conflicts: # requirements-dev.txt --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6760e1481b..1a0ab3c93d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ pytest-asyncio==1.3.0 pytest-cov==6.1.1 pytest-mock==3.14.1 pytest==9.0.1 -ruff==0.11.8 +ruff==0.15.10 types-Authlib==1.5.0.20250516 types-fpdf2==2.8.3.20250516 types-psutil==7.0.0.20250601 From 3867c8172d3135664d2a559b270af0a1e288387d Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:25:42 +0200 Subject: [PATCH 10/28] Remove Ruff target-version to let requires-python take precedence --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c22bbfdc7c..03f1fc0d45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ minimal-titan-version-code = 158 # Same as Black. line-length = 88 indent-width = 4 -target-version = "py314" + [tool.ruff.lint] select = [ From 474a50c0d7a2d2fc65f916d8d2a85960f9287ee6 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:26:05 +0200 Subject: [PATCH 11/28] Format --- app/core/users/factory_users.py | 4 +--- app/modules/booking/endpoints_booking.py | 2 +- app/modules/cdr/utils_cdr.py | 4 ++-- app/modules/raid/cruds_raid.py | 8 +++++--- tests/core/test_users.py | 4 ++-- tests_script.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/core/users/factory_users.py b/app/core/users/factory_users.py index 3efc18058e..76708e4530 100644 --- a/app/core/users/factory_users.py +++ b/app/core/users/factory_users.py @@ -34,9 +34,7 @@ class CoreUsersFactory(Factory): @classmethod def init_demo_users(cls, settings: Settings) -> None: cls.demo_users = ( - settings.FACTORIES_DEMO_USERS - if settings.FACTORIES_DEMO_USERS - else [ + settings.FACTORIES_DEMO_USERS or [ UserDemoFactoryConfig( firstname="Alice", name="Dupont", diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index 5db723bfba..0df7af34a6 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -302,7 +302,7 @@ async def create_booking( group = await cruds_groups.get_group_by_id(group_id=manager.group_id, db=db) local_start = result.start.astimezone(ZoneInfo("Europe/Paris")) - applicant_nickname = user.nickname if user.nickname else user.firstname + applicant_nickname = user.nickname or user.firstname content = f"{applicant_nickname} - {result.room.name} {local_start.strftime('%m/%d/%Y, %H:%M')} - {result.reason}" # Setting time to Paris timezone in order to have the correct time in the notification diff --git a/app/modules/cdr/utils_cdr.py b/app/modules/cdr/utils_cdr.py index 1fb940ba97..a29a781f88 100644 --- a/app/modules/cdr/utils_cdr.py +++ b/app/modules/cdr/utils_cdr.py @@ -378,7 +378,7 @@ def build_data_rows( row: list[str | int] = [""] * col_idx row[0] = user.name row[1] = user.firstname - row[2] = user.nickname if user.nickname else "" + row[2] = user.nickname or "" row[3] = user.email answers = users_answers.get(user.id, []) @@ -389,7 +389,7 @@ def build_data_rows( for prod_struct in product_structure: for vinfo in prod_struct["variants_info"]: - p = purchases_map.get(vinfo["variant"].id, None) + p = purchases_map.get(vinfo["variant"].id) if p and p.quantity > 0: row[vinfo["qty_col"]] = p.quantity if ( diff --git a/app/modules/raid/cruds_raid.py b/app/modules/raid/cruds_raid.py index 8a2c8ea40d..7a464befd4 100644 --- a/app/modules/raid/cruds_raid.py +++ b/app/modules/raid/cruds_raid.py @@ -563,9 +563,11 @@ async def get_number_of_team_by_difficulty( team_numbers = [ team.number if team.number is not None and team.number >= 0 else 0 for team in filter( - lambda team: team.validation_progress == 100 - and team.number is not None - and team.number >= 0, + lambda team: ( + team.validation_progress == 100 + and team.number is not None + and team.number >= 0 + ), teams_found, ) ] diff --git a/tests/core/test_users.py b/tests/core/test_users.py index 0a711a95c8..a79761e8ce 100644 --- a/tests/core/test_users.py +++ b/tests/core/test_users.py @@ -234,7 +234,7 @@ def test_create_and_activate_user( "activation_token": UNIQUE_TOKEN, "password": "password", "firstname": "firstname", - "name": email.split("@")[0], + "name": email.split("@", maxsplit=1)[0], "nickname": "nickname", "floor": "X1", }, @@ -246,7 +246,7 @@ def test_create_and_activate_user( "/users/", headers={"Authorization": f"Bearer {token_admin_user}"}, ) - user = next(user for user in users.json() if user["name"] == email.split("@")[0]) + user = next(user for user in users.json() if user["name"] == email.split("@", maxsplit=1)[0]) assert user is not None assert user["account_type"] == expected_account_type.value diff --git a/tests_script.py b/tests_script.py index f911da8e99..3ce1a394ec 100644 --- a/tests_script.py +++ b/tests_script.py @@ -22,7 +22,7 @@ def get_changed_files(): """Enumerate files changed compared to main branch.""" # We use git diff to get the list of changed files with three dots (...) to compare with the base commit of the PR in the main branch - diff = subprocess.check_output( # noqa: S603 + diff = subprocess.check_output( ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 text=True, ).strip() From bc406701dff9c098f2a3cfb44335223f042f0df9 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:33:10 +0200 Subject: [PATCH 12/28] Fix lint # Conflicts: # tests/core/test_utils.py --- app/core/google_api/google_api.py | 3 ++- app/core/groups/groups_type.py | 6 ++--- app/core/mypayment/types_mypayment.py | 18 +++++++------- app/core/users/factory_users.py | 34 +++++++++++++-------------- tests/core/test_users.py | 4 +++- tests/core/test_utils.py | 2 +- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/core/google_api/google_api.py b/app/core/google_api/google_api.py index 2d3517f1b9..0575ef9e95 100644 --- a/app/core/google_api/google_api.py +++ b/app/core/google_api/google_api.py @@ -1,5 +1,6 @@ import logging from types import TracebackType +from typing import Self from fastapi import HTTPException, Request from google.auth.transport import requests @@ -217,7 +218,7 @@ def __init__(self, db: AsyncSession, settings: Settings): self._db = db self._settings = settings - async def __aenter__(self) -> "DriveGoogleAPI": + async def __aenter__(self) -> Self: google_api = GoogleAPI() creds = await google_api.get_credentials(self._db, self._settings) self._drive: Resource = build("drive", "v3", credentials=creds) diff --git a/app/core/groups/groups_type.py b/app/core/groups/groups_type.py index cf2652a18d..d675f7290d 100644 --- a/app/core/groups/groups_type.py +++ b/app/core/groups/groups_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class GroupType(str, Enum): +class GroupType(StrEnum): """ In Hyperion, each user may have multiple groups. Belonging to a group gives access to a set of specific permissions. @@ -14,7 +14,7 @@ class GroupType(str, Enum): admin = "0a25cb76-4b63-4fd3-b939-da6d9feabf28" -class AccountType(str, Enum): +class AccountType(StrEnum): """ Various account types that can be created in Hyperion. Each account type is associated with a set of permissions. diff --git a/app/core/mypayment/types_mypayment.py b/app/core/mypayment/types_mypayment.py index c526982b25..0e0f5e3959 100644 --- a/app/core/mypayment/types_mypayment.py +++ b/app/core/mypayment/types_mypayment.py @@ -1,25 +1,25 @@ -from enum import Enum +from enum import StrEnum from uuid import UUID -class WalletType(str, Enum): +class WalletType(StrEnum): USER = "user" STORE = "store" -class WalletDeviceStatus(str, Enum): +class WalletDeviceStatus(StrEnum): INACTIVE = "inactive" ACTIVE = "active" REVOKED = "revoked" -class TransactionType(str, Enum): +class TransactionType(StrEnum): DIRECT = "direct" REQUEST = "request" REFUND = "refund" -class HistoryType(str, Enum): +class HistoryType(StrEnum): TRANSFER = "transfer" RECEIVED = "received" GIVEN = "given" @@ -27,7 +27,7 @@ class HistoryType(str, Enum): REFUND_DEBITED = "refund_debited" -class TransactionStatus(str, Enum): +class TransactionStatus(StrEnum): """ CONFIRMED: The transaction has been confirmed and is complete. CANCELED: The transaction has been canceled. It is used for transfer requests, for which the user has 15 minutes to complete the HelloAsso checkout @@ -41,17 +41,17 @@ class TransactionStatus(str, Enum): PENDING = "pending" -class RequestStatus(str, Enum): +class RequestStatus(StrEnum): PROPOSED = "proposed" ACCEPTED = "accepted" REFUSED = "refused" -class TransferType(str, Enum): +class TransferType(StrEnum): HELLO_ASSO = "hello_asso" -class ActionType(str, Enum): +class ActionType(StrEnum): TRANSFER = "transfer" REFUND = "refund" CANCEL = "cancel" diff --git a/app/core/users/factory_users.py b/app/core/users/factory_users.py index 76708e4530..0f51db4e04 100644 --- a/app/core/users/factory_users.py +++ b/app/core/users/factory_users.py @@ -33,24 +33,22 @@ class CoreUsersFactory(Factory): @classmethod def init_demo_users(cls, settings: Settings) -> None: - cls.demo_users = ( - settings.FACTORIES_DEMO_USERS or [ - UserDemoFactoryConfig( - firstname="Alice", - name="Dupont", - nickname="alice", - email="demo1@test.fr", - password=Faker().password(16, True, True, True, True), - ), - UserDemoFactoryConfig( - firstname="Bob", - name="Martin", - nickname="bob", - email="demo2@test.fr", - password=Faker().password(16, True, True, True, True), - ), - ] - ) + cls.demo_users = settings.FACTORIES_DEMO_USERS or [ + UserDemoFactoryConfig( + firstname="Alice", + name="Dupont", + nickname="alice", + email="demo1@test.fr", + password=Faker().password(16, True, True, True, True), + ), + UserDemoFactoryConfig( + firstname="Bob", + name="Martin", + nickname="bob", + email="demo2@test.fr", + password=Faker().password(16, True, True, True, True), + ), + ] cls.demo_users_id = [str(uuid.uuid4()) for _ in cls.demo_users] @classmethod diff --git a/tests/core/test_users.py b/tests/core/test_users.py index a79761e8ce..b6be304d5c 100644 --- a/tests/core/test_users.py +++ b/tests/core/test_users.py @@ -246,7 +246,9 @@ def test_create_and_activate_user( "/users/", headers={"Authorization": f"Bearer {token_admin_user}"}, ) - user = next(user for user in users.json() if user["name"] == email.split("@", maxsplit=1)[0]) + user = next( + user for user in users.json() if user["name"] == email.split("@", maxsplit=1)[0] + ) assert user is not None assert user["account_type"] == expected_account_type.value diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 54cbd09b52..0e02830489 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -83,7 +83,7 @@ async def test_save_file() -> None: async def test_save_file_with_invalid_content_type() -> None: valid_uuid = str(uuid.uuid4()) with ( - pytest.raises(HTTPException, match="400: Invalid file format, supported*"), + pytest.raises(HTTPException, match=r"400: Invalid file format, supported*"), pathlib.Path("assets/images/default_profile_picture.png").open("rb") as file, ): await save_file_as_data( From 2fa146cb9a52eca032e477d7d146f58dc0ff376d Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:06:05 +0200 Subject: [PATCH 13/28] Remove circular import in module permissions --- app/core/permissions/endpoints_permissions.py | 6 +----- app/module.py | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/core/permissions/endpoints_permissions.py b/app/core/permissions/endpoints_permissions.py index ad427c8014..7398c15217 100644 --- a/app/core/permissions/endpoints_permissions.py +++ b/app/core/permissions/endpoints_permissions.py @@ -17,6 +17,7 @@ is_user, is_user_in, ) +from app.module import full_name_permissions_list, permissions_list from app.types.module import CoreModule from app.utils.tools import is_group_id_valid @@ -46,7 +47,6 @@ async def read_permissions_list( """ Return all permissions from database """ - from app.module import full_name_permissions_list return full_name_permissions_list @@ -63,7 +63,6 @@ async def read_permissions( """ Return all permissions from database """ - from app.module import permissions_list return await cruds_permissions.get_permissions(permissions_list, db) @@ -81,7 +80,6 @@ async def read_permission( """ Return permission with name from database """ - from app.module import permissions_list if permission_name not in permissions_list: raise HTTPException( @@ -112,7 +110,6 @@ async def create_permission( """ Create a new permission in database """ - from app.module import permissions_list if permission.permission_name not in permissions_list: raise HTTPException( @@ -144,7 +141,6 @@ async def delete_permission( """ Delete a permission from database by name """ - from app.module import permissions_list if permission.permission_name not in permissions_list: raise HTTPException( diff --git a/app/module.py b/app/module.py index ed0f9d305f..468e694221 100644 --- a/app/module.py +++ b/app/module.py @@ -10,6 +10,8 @@ module_list: list[Module] = [] core_module_list: list[CoreModule] = [] all_modules: list[CoreModule] = [] +permissions_list: list[str] = [] +full_name_permissions_list: list[str] = [] for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): endpoint_module = importlib.import_module( @@ -39,10 +41,6 @@ ) -permissions_list: list[str] = [] -full_name_permissions_list: list[str] = [] - - class DuplicatePermissionsError(Exception): def __init__(self, permissions: list[list[str]]): arranged_permissions = [ From a0b20b0a03180d35e860e61412c62f2e8c99ea86 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:07:56 +0200 Subject: [PATCH 14/28] Don't import module from tests in raid tests --- tests/modules/test_raid.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/modules/test_raid.py b/tests/modules/test_raid.py index f458a25cc8..e9f25dcf26 100644 --- a/tests/modules/test_raid.py +++ b/tests/modules/test_raid.py @@ -25,7 +25,7 @@ MeetingPlace, Size, ) -from app.modules.raid.utils.utils_raid import calculate_raid_payment +from app.modules.raid.utils.utils_raid import calculate_raid_payment, set_team_number from tests.commons import ( add_coredata_to_db, add_object_to_db, @@ -747,7 +747,6 @@ async def test_set_team_number_utility_empty_database( mock_update_team = mocker.patch("app.modules.raid.cruds_raid.update_team") # Call the function - from app.modules.raid.utils.utils_raid import set_team_number await set_team_number(mock_team, mock_db) @@ -780,7 +779,6 @@ async def test_set_team_number_utility_existing_teams( mock_update_team = mocker.patch("app.modules.raid.cruds_raid.update_team") # Call the function - from app.modules.raid.utils.utils_raid import set_team_number await set_team_number(mock_team, mock_db) @@ -807,7 +805,6 @@ async def test_set_team_number_utility_no_difficulty( mock_update_team = mocker.patch("app.modules.raid.cruds_raid.update_team") # Call the function - from app.modules.raid.utils.utils_raid import set_team_number await set_team_number(mock_team, mock_db) @@ -836,7 +833,6 @@ async def test_set_team_number_utility_discovery_difficulty( mock_update_team = mocker.patch("app.modules.raid.cruds_raid.update_team") # Call the function - from app.modules.raid.utils.utils_raid import set_team_number await set_team_number(mock_team, mock_db) From 0aa6990623354ab043c564de191749ec51cc4345 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:09:03 +0200 Subject: [PATCH 15/28] Use StrEnum --- app/core/permissions/type_permissions.py | 4 ++-- app/core/utils/log.py | 4 ++-- app/modules/amap/types_amap.py | 6 +++--- app/modules/booking/types_booking.py | 4 ++-- app/modules/calendar/types_calendar.py | 6 +++--- app/modules/campaign/types_campaign.py | 6 +++--- app/modules/cdr/types_cdr.py | 10 +++++----- app/modules/raffle/types_raffle.py | 4 ++-- app/modules/raid/raid_type.py | 14 +++++++------- app/types/content_type.py | 4 ++-- app/types/scopes_type.py | 4 ++-- app/types/websocket.py | 6 +++--- migrations/versions/21-fix_phonebook.py | 2 +- migrations/versions/28-booking_type.py | 4 ++-- migrations/versions/29-hardcode-BDS-group.py | 4 ++-- migrations/versions/30-membership.py | 6 +++--- migrations/versions/32-MyECLPay.py | 14 +++++++------- migrations/versions/35-topic-membership.py | 4 ++-- migrations/versions/49-permissions.py | 4 ++-- migrations/versions/51_remove_hardcoded_floor.py | 4 ++-- 20 files changed, 57 insertions(+), 57 deletions(-) diff --git a/app/core/permissions/type_permissions.py b/app/core/permissions/type_permissions.py index 3dc5e83dd6..e138c75ce7 100644 --- a/app/core/permissions/type_permissions.py +++ b/app/core/permissions/type_permissions.py @@ -1,5 +1,5 @@ -from enum import Enum +from enum import StrEnum -class ModulePermissions(str, Enum): +class ModulePermissions(StrEnum): pass diff --git a/app/core/utils/log.py b/app/core/utils/log.py index 07a74335e2..2d089f842c 100644 --- a/app/core/utils/log.py +++ b/app/core/utils/log.py @@ -1,7 +1,7 @@ import logging import logging.config import queue -from enum import Enum +from enum import StrEnum from logging.handlers import QueueHandler, QueueListener from pathlib import Path from typing import Any @@ -12,7 +12,7 @@ class ColoredConsoleFormatter(uvicorn.logging.DefaultFormatter): - class ConsoleColors(str, Enum): + class ConsoleColors(StrEnum): """Colors can be found here: https://talyian.github.io/ansicolors/""" DEBUG = "\033[38;5;12m" diff --git a/app/modules/amap/types_amap.py b/app/modules/amap/types_amap.py index e08f6d901d..5265133a8f 100644 --- a/app/modules/amap/types_amap.py +++ b/app/modules/amap/types_amap.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class AmapSlotType(str, Enum): +class AmapSlotType(StrEnum): midi = "midi" soir = "soir" @@ -9,7 +9,7 @@ def __str__(self) -> str: return f"{self.name}<{self.value}" -class DeliveryStatusType(str, Enum): +class DeliveryStatusType(StrEnum): creation = "creation" # Can edit date, add and remove products, no order possible orderable = "orderable" # Ordering is possible, no edition possible locked = "locked" # Can't order diff --git a/app/modules/booking/types_booking.py b/app/modules/booking/types_booking.py index e8f2187c17..aab919c13b 100644 --- a/app/modules/booking/types_booking.py +++ b/app/modules/booking/types_booking.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class Decision(str, Enum): +class Decision(StrEnum): approved = "approved" declined = "declined" pending = "pending" diff --git a/app/modules/calendar/types_calendar.py b/app/modules/calendar/types_calendar.py index 8f05b29cb9..44959bb779 100644 --- a/app/modules/calendar/types_calendar.py +++ b/app/modules/calendar/types_calendar.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class CalendarEventType(str, Enum): +class CalendarEventType(StrEnum): eventAE = "Event AE" eventUSE = "Event USE" independentAssociation = "Asso indé" @@ -14,7 +14,7 @@ def __str__(self) -> str: return f"{self.name}<{self.value}" -class Decision(str, Enum): +class Decision(StrEnum): approved = "approved" declined = "declined" pending = "pending" diff --git a/app/modules/campaign/types_campaign.py b/app/modules/campaign/types_campaign.py index 825c79b374..4ade76ffac 100644 --- a/app/modules/campaign/types_campaign.py +++ b/app/modules/campaign/types_campaign.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class ListType(str, Enum): +class ListType(StrEnum): """ A list can be "Serios" or "Pipo". There will also be one "Blank" list by section that will be automatically added when the vote is open. """ @@ -11,7 +11,7 @@ class ListType(str, Enum): blank = "Blank" -class StatusType(str, Enum): +class StatusType(StrEnum): """ Status of the voting """ diff --git a/app/modules/cdr/types_cdr.py b/app/modules/cdr/types_cdr.py index dabe756973..f0dc4bc028 100644 --- a/app/modules/cdr/types_cdr.py +++ b/app/modules/cdr/types_cdr.py @@ -1,12 +1,12 @@ -from enum import Enum +from enum import StrEnum -class DocumentSignatureType(str, Enum): +class DocumentSignatureType(StrEnum): material = "material" numeric = "numeric" -class PaymentType(str, Enum): +class PaymentType(StrEnum): cash = "cash" check = "check" helloasso = "HelloAsso" @@ -14,14 +14,14 @@ class PaymentType(str, Enum): archived = "archived" -class CdrStatus(str, Enum): +class CdrStatus(StrEnum): pending = "pending" online = "online" onsite = "onsite" closed = "closed" -class CdrLogActionType(str, Enum): +class CdrLogActionType(StrEnum): purchase_add = "purchase_add" purchase_delete = "purchase_delete" payment_add = "payment_add" diff --git a/app/modules/raffle/types_raffle.py b/app/modules/raffle/types_raffle.py index 0349f67d44..35e5357996 100644 --- a/app/modules/raffle/types_raffle.py +++ b/app/modules/raffle/types_raffle.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class RaffleStatusType(str, Enum): +class RaffleStatusType(StrEnum): creation = "creation" # Can edit every parameter open = "open" # Ordering is possible, no edition possible lock = "lock" # Can't order diff --git a/app/modules/raid/raid_type.py b/app/modules/raid/raid_type.py index d15b19ff36..d611ada07b 100644 --- a/app/modules/raid/raid_type.py +++ b/app/modules/raid/raid_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class DocumentType(str, Enum): +class DocumentType(StrEnum): idCard = "idCard" # the id card of the participant medicalCertificate = ( "medicalCertificate" # the medical certificate of the participant @@ -11,7 +11,7 @@ class DocumentType(str, Enum): parentAuthorization = "parentAuthorization" # the parent authorization -class Size(str, Enum): # for the T-shirt and the bike +class Size(StrEnum): # for the T-shirt and the bike XS = "XS" S = "S" M = "M" @@ -20,26 +20,26 @@ class Size(str, Enum): # for the T-shirt and the bike None_ = "None" -class MeetingPlace(str, Enum): # place of meeting for the raid +class MeetingPlace(StrEnum): # place of meeting for the raid centrale = "centrale" bellecour = "bellecour" anyway = "anyway" -class Difficulty(str, Enum): # the difficulty of the raid +class Difficulty(StrEnum): # the difficulty of the raid discovery = "discovery" sports = "sports" expert = "expert" -class Situation(str, Enum): # the situation of the participant +class Situation(StrEnum): # the situation of the participant centrale = "centrale" otherSchool = "otherSchool" corporatePartner = "corporatePartner" other = "other" -class DocumentValidation(str, Enum): +class DocumentValidation(StrEnum): pending = "pending" accepted = "accepted" refused = "refused" diff --git a/app/types/content_type.py b/app/types/content_type.py index e82c6356dd..80cf455476 100644 --- a/app/types/content_type.py +++ b/app/types/content_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class ContentType(str, Enum): +class ContentType(StrEnum): """ Accepted `content_type` for files """ diff --git a/app/types/scopes_type.py b/app/types/scopes_type.py index 6ecf7db490..b56b18b626 100644 --- a/app/types/scopes_type.py +++ b/app/types/scopes_type.py @@ -1,7 +1,7 @@ -from enum import Enum +from enum import StrEnum -class ScopeType(str, Enum): +class ScopeType(StrEnum): """ Various scopes that can be included in JWT token """ diff --git a/app/types/websocket.py b/app/types/websocket.py index b51c0d335f..d7b70079dd 100644 --- a/app/types/websocket.py +++ b/app/types/websocket.py @@ -1,7 +1,7 @@ import asyncio import logging import os -from enum import Enum +from enum import StrEnum from typing import Any, Literal from broadcaster import Broadcast @@ -15,7 +15,7 @@ from app.utils.auth import auth_utils -class HyperionWebsocketsRoom(str, Enum): +class HyperionWebsocketsRoom(StrEnum): CDR = "5a816d32-8b5d-4c44-8a8d-18fd830ec5a8" @@ -27,7 +27,7 @@ class WSMessageModel(BaseModel): data: Any -class ConnectionWSMessageModelStatus(str, Enum): +class ConnectionWSMessageModelStatus(StrEnum): connected = "connected" invalid = "invalid_token" diff --git a/migrations/versions/21-fix_phonebook.py b/migrations/versions/21-fix_phonebook.py index 6149c304d7..afc365be6d 100644 --- a/migrations/versions/21-fix_phonebook.py +++ b/migrations/versions/21-fix_phonebook.py @@ -50,7 +50,7 @@ def define_order_of_memberships(memberships: list[sa.Row[Any]]) -> list[list]: for membership in memberships: if membership[2]: tags = membership[2].split(";") - tags.sort(key=lambda x: member_order.index(x)) + tags.sort(key=member_order.index) else: tags = ["Default"] memberships2.append( diff --git a/migrations/versions/28-booking_type.py b/migrations/versions/28-booking_type.py index fe7555b19b..3b14ff894d 100644 --- a/migrations/versions/28-booking_type.py +++ b/migrations/versions/28-booking_type.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from datetime import datetime -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -21,7 +21,7 @@ depends_on: str | Sequence[str] | None = None -class Decision(str, Enum): +class Decision(StrEnum): approved = "approved" declined = "declined" pending = "pending" diff --git a/migrations/versions/29-hardcode-BDS-group.py b/migrations/versions/29-hardcode-BDS-group.py index f52f8fc0f3..02a3ff065d 100644 --- a/migrations/versions/29-hardcode-BDS-group.py +++ b/migrations/versions/29-hardcode-BDS-group.py @@ -4,7 +4,7 @@ """ from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING from uuid import uuid4 @@ -23,7 +23,7 @@ depends_on: str | Sequence[str] | None = None -class GroupType(str, Enum): +class GroupType(StrEnum): # Core groups admin = "0a25cb76-4b63-4fd3-b939-da6d9feabf28" AE = "45649735-866a-49df-b04b-a13c74fd5886" diff --git a/migrations/versions/30-membership.py b/migrations/versions/30-membership.py index ef31b2291e..ab7009180b 100644 --- a/migrations/versions/30-membership.py +++ b/migrations/versions/30-membership.py @@ -6,7 +6,7 @@ import uuid from collections.abc import Sequence from datetime import date -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING from app.core.schools.schools_type import SchoolType @@ -24,7 +24,7 @@ depends_on: str | Sequence[str] | None = None -class AvailableAssociationMembership(str, Enum): +class AvailableAssociationMembership(StrEnum): aeecl = "AEECL" useecl = "USEECL" @@ -81,7 +81,7 @@ class AvailableAssociationMembership(str, Enum): USEECL_ID = uuid.uuid4() -class GroupType(str, Enum): +class GroupType(StrEnum): # Core groups admin = "0a25cb76-4b63-4fd3-b939-da6d9feabf28" AE = "45649735-866a-49df-b04b-a13c74fd5886" diff --git a/migrations/versions/32-MyECLPay.py b/migrations/versions/32-MyECLPay.py index 3b4ac1cdae..8026e20706 100644 --- a/migrations/versions/32-MyECLPay.py +++ b/migrations/versions/32-MyECLPay.py @@ -4,7 +4,7 @@ """ from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -22,37 +22,37 @@ depends_on: str | Sequence[str] | None = None -class TransactionType(str, Enum): +class TransactionType(StrEnum): DIRECT = "direct" REQUEST = "request" REFUND = "refund" -class WalletType(str, Enum): +class WalletType(StrEnum): USER = "user" STORE = "store" -class TransferType(str, Enum): +class TransferType(StrEnum): HELLO_ASSO = "hello_asso" CHECK = "check" CASH = "cash" BANK_TRANSFER = "bank_transfer" -class WalletDeviceStatus(str, Enum): +class WalletDeviceStatus(StrEnum): INACTIVE = "inactive" ACTIVE = "active" REVOKED = "revoked" -class TransactionStatus(str, Enum): +class TransactionStatus(StrEnum): CONFIRMED = "confirmed" CANCELED = "canceled" REFUNDED = "refunded" -class RequestStatus(str, Enum): +class RequestStatus(StrEnum): PROPOSED = "proposed" ACCEPTED = "accepted" REFUSED = "refused" diff --git a/migrations/versions/35-topic-membership.py b/migrations/versions/35-topic-membership.py index 6a124433ac..2aaccfa0db 100644 --- a/migrations/versions/35-topic-membership.py +++ b/migrations/versions/35-topic-membership.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING import sqlalchemy as sa @@ -24,7 +24,7 @@ depends_on: str | Sequence[str] | None = None -class Topic(str, Enum): +class Topic(StrEnum): cinema = "cinema" advert = "advert" amap = "amap" diff --git a/migrations/versions/49-permissions.py b/migrations/versions/49-permissions.py index dabad54844..cb87893163 100644 --- a/migrations/versions/49-permissions.py +++ b/migrations/versions/49-permissions.py @@ -4,7 +4,7 @@ """ from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -21,7 +21,7 @@ depends_on: str | Sequence[str] | None = None -class AccountType(str, Enum): +class AccountType(StrEnum): """ Various account types that can be created in Hyperion. Each account type is associated with a set of permissions. diff --git a/migrations/versions/51_remove_hardcoded_floor.py b/migrations/versions/51_remove_hardcoded_floor.py index e904376d62..ea51817c29 100644 --- a/migrations/versions/51_remove_hardcoded_floor.py +++ b/migrations/versions/51_remove_hardcoded_floor.py @@ -4,7 +4,7 @@ """ from collections.abc import Sequence -from enum import Enum +from enum import StrEnum from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -20,7 +20,7 @@ depends_on: str | Sequence[str] | None = None -class FloorsType(str, Enum): +class FloorsType(StrEnum): # WARNING: the key is used in the database. Use the same key and value. Autre = "Autre" Adoma = "Adoma" From a2f65b594907463008c13c8a1a15e7251e4dc85b Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:11:53 +0200 Subject: [PATCH 16/28] Use Special type parameter syntax https://docs.astral.sh/ruff/rules/non-pep695-generic-function/ --- app/utils/initialization.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/utils/initialization.py b/app/utils/initialization.py index d7257e22ad..a1672be9e2 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -202,10 +202,7 @@ def delete_core_data_crud_sync(schema: str, db: Session) -> None: db.commit() -CoreDataClass = TypeVar("CoreDataClass", bound=core_data.BaseCoreData) - - -def get_core_data_sync( +def get_core_data_sync[CoreDataClass: core_data.BaseCoreData]( core_data_class: type[CoreDataClass], db: Session, ) -> CoreDataClass: @@ -292,11 +289,7 @@ def drop_db_sync(conn: Connection): my_metadata.drop_all(bind=conn) -P = ParamSpec("P") -R = TypeVar("R") - - -async def use_lock_for_workers( +async def use_lock_for_workers[**P, R]( job_function: Callable[P, R], key: str, redis_client: redis.Redis | None, From e9b83fb39939d62dfffd43a34df593367a489b0c Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:12:35 +0200 Subject: [PATCH 17/28] Use Special type parameter syntax --- app/utils/initialization.py | 1 - app/utils/tools.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/utils/initialization.py b/app/utils/initialization.py index a1672be9e2..4c880567c9 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -2,7 +2,6 @@ import logging import os from collections.abc import Callable -from typing import ParamSpec, TypeVar import psutil import redis diff --git a/app/utils/tools.py b/app/utils/tools.py index 9b9e73b55f..a70e835f4f 100644 --- a/app/utils/tools.py +++ b/app/utils/tools.py @@ -7,7 +7,7 @@ import unicodedata from collections.abc import Callable, Sequence from inspect import iscoroutinefunction -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any from uuid import UUID import calypsso @@ -467,10 +467,7 @@ def get_random_string(length: int = 5) -> str: ) -CoreDataClass = TypeVar("CoreDataClass", bound=core_data.BaseCoreData) - - -async def get_core_data( +async def get_core_data[CoreDataClass: core_data.BaseCoreData]( core_data_class: type[CoreDataClass], db: AsyncSession, ) -> CoreDataClass: From 72a3cd5e4de8e9e78865cf599e9987a6a86c54b6 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:12:41 +0200 Subject: [PATCH 18/28] Sort imports --- app/core/utils/config.py | 2 +- app/modules/booking/endpoints_booking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 7e7759e504..8c95fb6aa6 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -1,10 +1,10 @@ import pathlib +import tomllib from functools import cached_property from re import Pattern from typing import Any, ClassVar import jwt -import tomllib from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_pem_private_key from pydantic import BaseModel, computed_field, model_validator diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index 0df7af34a6..3f6b4279a7 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -1,10 +1,10 @@ import logging import uuid from datetime import UTC, datetime +from zoneinfo import ZoneInfo from fastapi import Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from zoneinfo import ZoneInfo from app.core.groups import cruds_groups from app.core.groups.groups_type import AccountType From c3b349fe7d00762f1c5d6ffbf5794dd1a00d0764 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:14:14 +0200 Subject: [PATCH 19/28] Ignore weasyprint import --- app/utils/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/utils/tools.py b/app/utils/tools.py index a70e835f4f..c6afd43d0d 100644 --- a/app/utils/tools.py +++ b/app/utils/tools.py @@ -382,7 +382,8 @@ async def generate_pdf_from_template( You should only provide thrusted templates to this function. See [WeasyPrint security consideration](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#security) """ - from weasyprint import CSS, HTML + # We only import Weasyprint here to be able to launch Hyperion without installing the module + from weasyprint import CSS, HTML # noqa: PLC0415 templates = Environment( loader=FileSystemLoader("assets/templates"), From fc04d2ca81cae40a56b1230327637f0547438d26 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:14:10 +0200 Subject: [PATCH 20/28] Add ty==0.0.29 # Conflicts: # requirements-dev.txt --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1a0ab3c93d..57a484726e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,6 +9,7 @@ pytest-cov==6.1.1 pytest-mock==3.14.1 pytest==9.0.1 ruff==0.15.10 +ty==0.0.29 types-Authlib==1.5.0.20250516 types-fpdf2==2.8.3.20250516 types-psutil==7.0.0.20250601 From fbd46188efd69525e4eed09ed5d07a511c4f87ee Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:14:36 +0200 Subject: [PATCH 21/28] Fix type --- app/core/payment/payment_tool.py | 2 +- app/utils/initialization.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/core/payment/payment_tool.py b/app/core/payment/payment_tool.py index a2f4a95e72..a0d2a2ca15 100644 --- a/app/core/payment/payment_tool.py +++ b/app/core/payment/payment_tool.py @@ -162,7 +162,7 @@ async def init_checkout( # Thus we catch any exception and log it, then reraise it try: payer: HelloAssoApiV5ModelsCartsCheckoutPayer | None = None - if payer_user: + if payer_user is not None: payer = HelloAssoApiV5ModelsCartsCheckoutPayer( firstName=payer_user.firstname, lastName=payer_user.name, diff --git a/app/utils/initialization.py b/app/utils/initialization.py index 4c880567c9..4def97ebe4 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -1,6 +1,7 @@ import asyncio import logging import os +import uuid from collections.abc import Callable import psutil @@ -146,7 +147,7 @@ def set_core_data_crud_sync( def get_school_by_id_sync( - school_id: str, + school_id: uuid.UUID, db: Session, ) -> models_schools.CoreSchool | None: """ From 7975b921f8c866c7918fb7d7cbcfa9952aa8e5a7 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:14:30 +0200 Subject: [PATCH 22/28] Ignore Google API --- app/core/google_api/google_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/core/google_api/google_api.py b/app/core/google_api/google_api.py index 0575ef9e95..aabf34a445 100644 --- a/app/core/google_api/google_api.py +++ b/app/core/google_api/google_api.py @@ -246,7 +246,7 @@ def create_folder(self, folder_name: str, parent_folder_id: GoogleId) -> GoogleI "mimeType": "application/vnd.google-apps.folder", "parents": [parent_folder_id], } - response = self._drive.files().create(body=file_metadata).execute() + response = self._drive.files().create(body=file_metadata).execute() # ty:ignore[unresolved-attribute] folder_id: GoogleId = response.get("id") return folder_id @@ -270,7 +270,7 @@ async def upload_file( mimetype=mimetype, ) response = ( - self._drive.files().create(body=file_metadata, media_body=media).execute() + self._drive.files().create(body=file_metadata, media_body=media).execute() # ty:ignore[unresolved-attribute] ) uploaded_file_id: GoogleId = response.get("id") return uploaded_file_id @@ -288,7 +288,7 @@ def replace_file( mimetype="application/pdf", ) response = ( - self._drive.files() + self._drive.files() # ty:ignore[unresolved-attribute] .update(fileId=file_id, body=file_metadata, media_body=media) .execute() ) @@ -296,4 +296,4 @@ def replace_file( return result def delete_file(self, file_id: GoogleId) -> None: - self._drive.files().delete(fileId=file_id).execute() + self._drive.files().delete(fileId=file_id).execute() # ty:ignore[unresolved-attribute] From 3fdf32f2c12ee91e9af92ac0eed1c8ce90b24457 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:15:34 +0200 Subject: [PATCH 23/28] Fix ty issues --- app/core/payment/endpoints_payment.py | 170 +++++++++--------- app/core/payment/payment_tool.py | 16 +- app/core/utils/log.py | 2 +- app/modules/amap/endpoints_amap.py | 102 +++++++++-- .../flappybird/endpoints_flappybird.py | 11 +- app/modules/loan/endpoints_loan.py | 26 ++- app/modules/raffle/endpoints_raffle.py | 16 +- .../seed_library/endpoints_seed_library.py | 2 +- .../utils/data_exporter/global_exporter.py | 2 +- .../school_participants_exporter.py | 2 +- app/types/websocket.py | 4 +- app/utils/initialization.py | 6 +- migrations/versions/26-account_types.py | 2 +- 13 files changed, 244 insertions(+), 117 deletions(-) diff --git a/app/core/payment/endpoints_payment.py b/app/core/payment/endpoints_payment.py index 9f31849f3e..37c0d7e0ce 100644 --- a/app/core/payment/endpoints_payment.py +++ b/app/core/payment/endpoints_payment.py @@ -2,15 +2,16 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Request -from helloasso_python.models.hello_asso_api_v5_models_api_notifications_api_notification_type import ( - HelloAssoApiV5ModelsApiNotificationsApiNotificationType, -) from pydantic import TypeAdapter, ValidationError from sqlalchemy.ext.asyncio import AsyncSession from app.core.payment import cruds_payment, models_payment, schemas_payment from app.core.payment.types_payment import ( + FormNotificationResultContent, NotificationResultContent, + OrderNotificationResultContent, + OrganizationNotificationResultContent, + PayementNotificationResultContent, ) from app.dependencies import get_db from app.module import all_modules @@ -62,95 +63,94 @@ async def webhook( status_code=400, detail="Could not validate the webhook body", ) - if ( - content.eventType - == HelloAssoApiV5ModelsApiNotificationsApiNotificationType.ORDER - ): - pass - if ( - content.eventType - == HelloAssoApiV5ModelsApiNotificationsApiNotificationType.PAYMENT - ): - # We may receive the webhook multiple times, we only want to save a CheckoutPayment - # in the database the first time - existing_checkout_payment_model = ( - await cruds_payment.get_checkout_payment_by_hello_asso_payment_id( - hello_asso_payment_id=content.data.id, - db=db, - ) - ) - if existing_checkout_payment_model is not None: - hyperion_error_logger.debug( - f"Payment: ignoring webhook call for helloasso checkout payment id {content.data.id} as it already exists in the database", + match content: + case OrganizationNotificationResultContent(): + pass + case OrderNotificationResultContent(): + pass + case FormNotificationResultContent(): + pass + case PayementNotificationResultContent(): + # We may receive the webhook multiple times, we only want to save a CheckoutPayment + # in the database the first time + existing_checkout_payment_model = ( + await cruds_payment.get_checkout_payment_by_hello_asso_payment_id( + hello_asso_payment_id=content.data.id, + db=db, + ) ) - return + if existing_checkout_payment_model is not None: + hyperion_error_logger.debug( + f"Payment: ignoring webhook call for helloasso checkout payment id {content.data.id} as it already exists in the database", + ) + return - # If no metadata are included, this should not be a checkout we initiated - if not checkout_metadata: - hyperion_error_logger.info( - "Payment: missing checkout_metadata", - ) - return + # If no metadata are included, this should not be a checkout we initiated + if not checkout_metadata: + hyperion_error_logger.info( + "Payment: missing checkout_metadata", + ) + return - checkout = await cruds_payment.get_checkout_by_id( - checkout_id=uuid.UUID(checkout_metadata.hyperion_checkout_id), - db=db, - ) - # If a metadata with a checkout was present in the request but we can not find the checkout, - # we should raise an error - if not checkout: - hyperion_error_logger.error( - f"Payment: could not find checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}) in database for payment HelloAsso payment_id: {content.data.id}", - ) - raise HTTPException( - status_code=400, - detail=f"Could not find checkout {checkout_metadata.hyperion_checkout_id} in database", + checkout = await cruds_payment.get_checkout_by_id( + checkout_id=uuid.UUID(checkout_metadata.hyperion_checkout_id), + db=db, ) + # If a metadata with a checkout was present in the request but we can not find the checkout, + # we should raise an error + if not checkout: + hyperion_error_logger.error( + f"Payment: could not find checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}) in database for payment HelloAsso payment_id: {content.data.id}", + ) + raise HTTPException( + status_code=400, + detail=f"Could not find checkout {checkout_metadata.hyperion_checkout_id} in database", + ) - if checkout.secret != checkout_metadata.secret: - hyperion_error_logger.error( - f"Payment: secret mismatch for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})", + if checkout.secret != checkout_metadata.secret: + hyperion_error_logger.error( + f"Payment: secret mismatch for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})", + ) + raise HTTPException( + status_code=400, + detail="Secret mismatch", + ) + + checkout_payment_model = models_payment.CheckoutPayment( + id=uuid.uuid4(), + checkout_id=checkout.id, + paid_amount=content.data.amount, + tip_amount=content.data.amountTip, + hello_asso_payment_id=content.data.id, ) - raise HTTPException( - status_code=400, - detail="Secret mismatch", + await cruds_payment.create_checkout_payment( + checkout_payment=checkout_payment_model, + db=db, ) - checkout_payment_model = models_payment.CheckoutPayment( - id=uuid.uuid4(), - checkout_id=checkout.id, - paid_amount=content.data.amount, - tip_amount=content.data.amountTip, - hello_asso_payment_id=content.data.id, - ) - await cruds_payment.create_checkout_payment( - checkout_payment=checkout_payment_model, - db=db, - ) - - hyperion_error_logger.info( - f"Payment: checkout payment added to db for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})", - ) + hyperion_error_logger.info( + f"Payment: checkout payment added to db for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id})", + ) - # If a callback is defined for the module, we want to call it - try: - for module in all_modules: - if module.root == checkout.module: - if module.payment_callback is not None: - hyperion_error_logger.info( - f"Payment: calling module {checkout.module} payment callback", - ) - checkout_payment_schema = ( - schemas_payment.CheckoutPayment.model_validate( - checkout_payment_model.__dict__, + # If a callback is defined for the module, we want to call it + try: + for module in all_modules: + if module.root == checkout.module: + if module.payment_callback is not None: + hyperion_error_logger.info( + f"Payment: calling module {checkout.module} payment callback", ) - ) - await module.payment_callback(checkout_payment_schema, db) - hyperion_error_logger.info( - f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) succeeded", - ) - return - except Exception: - hyperion_error_logger.exception( - f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) failed", - ) + checkout_payment_schema = ( + schemas_payment.CheckoutPayment.model_validate( + checkout_payment_model.__dict__, + ) + ) + await module.payment_callback(checkout_payment_schema, db) + hyperion_error_logger.info( + f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) succeeded", + ) + return + except Exception: + hyperion_error_logger.exception( + f"Payment: call to module {checkout.module} payment callback for checkout (hyperion_checkout_id: {checkout_metadata.hyperion_checkout_id}, HelloAsso checkout_id: {checkout.id}) failed", + ) diff --git a/app/core/payment/payment_tool.py b/app/core/payment/payment_tool.py index a0d2a2ca15..b7bb7efa10 100644 --- a/app/core/payment/payment_tool.py +++ b/app/core/payment/payment_tool.py @@ -164,10 +164,16 @@ async def init_checkout( payer: HelloAssoApiV5ModelsCartsCheckoutPayer | None = None if payer_user is not None: payer = HelloAssoApiV5ModelsCartsCheckoutPayer( - firstName=payer_user.firstname, - lastName=payer_user.name, + first_name=payer_user.firstname, + last_name=payer_user.name, email=payer_user.email, - dateOfBirth=payer_user.birthday, + date_of_birth=datetime.combine( + payer_user.birthday, + datetime.min.time(), + tzinfo=UTC, + ) + if payer_user.birthday + else None, ) checkout_model_id = uuid.uuid4() @@ -222,7 +228,7 @@ async def init_checkout( f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos", ) - if response and response.id: + if response and response.id and response.redirect_url: checkout_model = models_payment.Checkout( id=checkout_model_id, module=module, @@ -239,7 +245,7 @@ async def init_checkout( payment_url=response.redirect_url, ) hyperion_error_logger.error( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. No checkout id returned", + f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. No checkout id or redirect URL returned", ) raise MissingHelloAssoCheckoutIdError() # noqa: TRY301 diff --git a/app/core/utils/log.py b/app/core/utils/log.py index 2d089f842c..cd1c65daab 100644 --- a/app/core/utils/log.py +++ b/app/core/utils/log.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Any -import uvicorn +import uvicorn.logging from app.core.utils.config import Settings diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index 1526fc7e45..82c4ada6d9 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -11,6 +11,7 @@ from app.core.permissions.type_permissions import ModulePermissions from app.core.users import cruds_users, models_users, schemas_users from app.core.users.endpoints_users import read_user +from app.core.users.schemas_users import CoreUserSimple from app.dependencies import ( get_db, get_notification_tool, @@ -20,7 +21,9 @@ ) from app.modules.amap import cruds_amap, models_amap, schemas_amap from app.modules.amap.factory_amap import AmapFactory +from app.modules.amap.schemas_amap import ProductComplete from app.modules.amap.types_amap import DeliveryStatusType +from app.types.exceptions import ObjectExpectedInDbNotFoundError from app.types.module import Module from app.utils.communication.notifications import NotificationTool from app.utils.redis import locker_get, locker_set @@ -417,10 +420,33 @@ async def get_order_by_id( products = await cruds_amap.get_products_of_order(db=db, order_id=order_id) return schemas_amap.OrderReturn( - productsdetail=products, delivery_name=order.delivery.name, + productsdetail=[ + schemas_amap.ProductQuantity( + quantity=product.quantity, + product=ProductComplete( + name=product.product.name, + price=product.product.price, + category=product.product.category, + id=product.product.id, + ), + ) + for product in products + ], + user=schemas_users.CoreUserSimple( + id=order.user.id, + name=order.user.name, + firstname=order.user.firstname, + nickname=order.user.nickname, + school_id=order.user.school_id, + account_type=order.user.account_type, + ), + delivery_id=order.delivery_id, + collection_slot=order.collection_slot, + order_id=order.order_id, + amount=order.amount, + ordering_date=order.ordering_date, delivery_date=order.delivery.delivery_date, - **order.__dict__, ) @@ -539,13 +565,39 @@ async def add_order_to_delievery( ) if orderret is None: - raise HTTPException(status_code=404, detail="added order not found") + raise ObjectExpectedInDbNotFoundError( + object_name="Order", + object_id=db_order.order_id, + ) return schemas_amap.OrderReturn( - productsdetail=productsret, - delivery_name=orderret.delivery.name, + user=CoreUserSimple( + id=orderret.user.id, + account_type=orderret.user.account_type, + school_id=orderret.user.school_id, + name=orderret.user.name, + firstname=orderret.user.firstname, + nickname=orderret.user.nickname, + ), + productsdetail=[ + schemas_amap.ProductQuantity( + quantity=product.quantity, + product=ProductComplete( + name=product.product.name, + price=product.product.price, + category=product.product.category, + id=product.product.id, + ), + ) + for product in productsret + ], + delivery_id=orderret.delivery_id, + collection_slot=orderret.collection_slot, + order_id=orderret.order_id, + amount=orderret.amount, + ordering_date=orderret.ordering_date, delivery_date=orderret.delivery.delivery_date, - **orderret.__dict__, + delivery_name=orderret.delivery.name, ) finally: locker_set(redis_client=redis_client, key=redis_key, lock=False) @@ -610,7 +662,7 @@ async def edit_order_from_delivery( ): raise HTTPException(status_code=400, detail="Invalid request") - amount = 0.0 + amount = 0 for product_id, product_quantity in zip( order.products_ids, order.products_quantity, @@ -624,11 +676,16 @@ async def edit_order_from_delivery( db_order = schemas_amap.OrderComplete( order_id=order_id, ordering_date=previous_order.ordering_date, - delivery_date=delivery.delivery_date, delivery_id=previous_order.delivery_id, user_id=previous_order.user_id, amount=amount, - **order.model_dump(), + products_ids=order.products_ids, + collection_slot=order.collection_slot + if order.collection_slot is not None + else previous_order.collection_slot, + products_quantity=order.products_quantity + if order.products_quantity is not None + else previous_order.products_quantity, ) previous_amount = previous_order.amount @@ -1059,10 +1116,33 @@ async def get_orders_of_user( raise HTTPException(status_code=404, detail="at least one order not found") res.append( schemas_amap.OrderReturn( - productsdetail=products, delivery_date=order.delivery.delivery_date, delivery_name=order.delivery.name, - **order.__dict__, + user=CoreUserSimple( + id=order.user.id, + account_type=order.user.account_type, + school_id=order.user.school_id, + name=order.user.name, + firstname=order.user.firstname, + nickname=order.user.nickname, + ), + productsdetail=[ + schemas_amap.ProductQuantity( + quantity=product.quantity, + product=ProductComplete( + name=product.product.name, + price=product.product.price, + category=product.product.category, + id=product.product.id, + ), + ) + for product in products + ], + delivery_id=order.delivery_id, + collection_slot=order.collection_slot, + order_id=order.order_id, + amount=order.amount, + ordering_date=order.ordering_date, ), ) return res diff --git a/app/modules/flappybird/endpoints_flappybird.py b/app/modules/flappybird/endpoints_flappybird.py index d1e68b2ade..c41e582e81 100644 --- a/app/modules/flappybird/endpoints_flappybird.py +++ b/app/modules/flappybird/endpoints_flappybird.py @@ -6,7 +6,7 @@ from app.core.groups.groups_type import AccountType from app.core.permissions.type_permissions import ModulePermissions -from app.core.users import models_users +from app.core.users import models_users, schemas_users from app.dependencies import get_db, is_user_allowed_to from app.modules.flappybird import ( cruds_flappybird, @@ -80,7 +80,14 @@ async def get_current_user_flappybird_personal_best( ) return schemas_flappybird.FlappyBirdScoreCompleteFeedBack( value=user_personal_best_table.value, - user=user_personal_best_table.user, + user=schemas_users.CoreUserSimple( + id=user_personal_best_table.user.id, + account_type=user_personal_best_table.user.account_type, + school_id=user_personal_best_table.user.school_id, + name=user_personal_best_table.user.name, + firstname=user_personal_best_table.user.firstname, + nickname=user_personal_best_table.user.nickname, + ), creation_time=user_personal_best_table.creation_time, position=position, ) diff --git a/app/modules/loan/endpoints_loan.py b/app/modules/loan/endpoints_loan.py index 06ff4eebf4..79158c3051 100644 --- a/app/modules/loan/endpoints_loan.py +++ b/app/modules/loan/endpoints_loan.py @@ -9,7 +9,7 @@ from app.core.groups.groups_type import AccountType from app.core.notification.schemas_notification import Message from app.core.permissions.type_permissions import ModulePermissions -from app.core.users import models_users +from app.core.users import models_users, schemas_users from app.dependencies import ( get_db, get_notification_tool, @@ -233,8 +233,28 @@ async def get_loans_by_loaner( loans.append( schemas_loan.Loan( items_qty=items_qty_ret, - loaner=loaner, - **loan.__dict__, + borrower_id=loan.borrower_id, + loaner_id=loan.loaner_id, + start=loan.start, + end=loan.end, + notes=loan.notes, + caution=loan.caution, + returned=loan.returned, + returned_date=loan.returned_date, + id=loan.id, + borrower=schemas_users.CoreUserSimple( + id=loan.borrower.id, + account_type=loan.borrower.account_type, + school_id=loan.borrower.school_id, + name=loan.borrower.name, + firstname=loan.borrower.firstname, + nickname=loan.borrower.nickname, + ), + loaner=schemas_loan.Loaner( + id=loan.loaner.id, + name=loan.loaner.name, + group_manager_id=loan.loaner.group_manager_id, + ), ), ) diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 75ea4573e7..c821039248 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -9,7 +9,7 @@ from app.core.groups import cruds_groups from app.core.groups.groups_type import AccountType from app.core.permissions.type_permissions import ModulePermissions -from app.core.users import cruds_users, models_users +from app.core.users import cruds_users, models_users, schemas_users from app.core.users.endpoints_users import read_user from app.dependencies import ( get_db, @@ -209,6 +209,7 @@ async def get_raffle_stats( amount_raised = sum( [ticket.pack_ticket.price / ticket.pack_ticket.pack_size for ticket in tickets], ) + amount_raised = int(amount_raised) return schemas_raffle.RaffleStats( tickets_sold=tickets_sold, @@ -901,7 +902,18 @@ async def get_cash_by_id( # We want to return a balance of 0 but we don't want to add it to the database # An admin AMAP has indeed to add a cash to the user the first time # TODO: this is a strange behaviour - return schemas_raffle.CashComplete(balance=0, user_id=user_id, user=user_db) + return schemas_raffle.CashComplete( + balance=0, + user_id=user_id, + user=schemas_users.CoreUserSimple( + id=user_db.id, + account_type=user_db.account_type, + school_id=user_db.school_id, + name=user_db.name, + firstname=user_db.firstname, + nickname=user_db.nickname, + ), + ) raise HTTPException( status_code=403, detail="Users that are not member of the group admin can only access the endpoint for their own user_id.", diff --git a/app/modules/seed_library/endpoints_seed_library.py b/app/modules/seed_library/endpoints_seed_library.py index 83bfb4a98c..e96a258c80 100644 --- a/app/modules/seed_library/endpoints_seed_library.py +++ b/app/modules/seed_library/endpoints_seed_library.py @@ -72,7 +72,7 @@ async def get_all_species_types( Return all available types of species from SpeciesType enum. """ return schemas_seed_library.SpeciesTypesReturn( - species_type=[species_type.value for species_type in SpeciesType], + species_type=list(SpeciesType), ) diff --git a/app/modules/sport_competition/utils/data_exporter/global_exporter.py b/app/modules/sport_competition/utils/data_exporter/global_exporter.py index 35c51c53f8..fd087106c5 100644 --- a/app/modules/sport_competition/utils/data_exporter/global_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/global_exporter.py @@ -64,7 +64,7 @@ def build_data_rows( data_rows: list[list[str | int]] = [] for user in users: user_purchases = users_purchases.get(user.user.id, []) - row: list[str | int] = [""] * col_idx # ty:ignore[invalid-assignment] + row: list[str | int] = [""] * col_idx row[0] = user.user.name row[1] = user.user.firstname row[2] = user.user.email diff --git a/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py b/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py index e723bb35be..b9700b992f 100644 --- a/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py @@ -74,7 +74,7 @@ def build_data_rows( data_rows: list[list[str | int]] = [] for user in users: user_purchases = users_purchases.get(user.user.id, []) - row: list[str | int] = [""] * col_idx # ty:ignore[invalid-assignment] + row: list[str | int] = [""] * col_idx row[0] = user.user.name row[1] = user.user.firstname row[2] = user.user.email diff --git a/app/types/websocket.py b/app/types/websocket.py index d7b70079dd..81ec37a23f 100644 --- a/app/types/websocket.py +++ b/app/types/websocket.py @@ -179,9 +179,9 @@ async def _subscribe_and_listen_to_channel(self, room_id: HyperionWebsocketsRoom f"Websocket: subscribed broadcaster to channel {room_id} for worker {os.getpid()}", ) - async for event in subscriber: # type: ignore[union-attr] # Should be fixed by https://github.com/encode/broadcaster/issues/136 + async for event in subscriber: # type: ignore[union-attr] # Should be fixed by https://github.com/encode/broadcaster/issues/136 # ty:ignore[not-iterable] await self._consume_events_from_broadcaster( - message_str=event.message, # type: ignore[union-attr] # Should be fixed by https://github.com/encode/broadcaster/issues/136 + message_str=event.message, # type: ignore[union-attr] # Should be fixed by https://github.com/encode/broadcaster/issues/136 # ty:ignore[unresolved-attribute] room_id=room_id, ) diff --git a/app/utils/initialization.py b/app/utils/initialization.py index 4def97ebe4..7d19fb2316 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -334,7 +334,7 @@ async def use_lock_for_workers[**P, R]( elif redis_client.set(key, "1", nx=True, ex=120): # We acquired the lock, we execute the function - logger.info(f"Running {job_function.__name__}") + logger.info(f"Running {getattr(job_function, '__name__', repr(job_function))}") await execute_async_or_sync_method(job_function, *args, **kwargs) @@ -353,7 +353,9 @@ async def use_lock_for_workers[**P, R]( elif unlock_key: # As an `unlock_key` is provided, we will wait until an other worker has finished executing `job_function` while redis_client.get(unlock_key) is None: - logger.debug(f"Waiting for {job_function.__name__} to finish") + logger.debug( + f"Waiting for {getattr(job_function, '__name__', repr(job_function))} to finish", + ) await asyncio.sleep(1) diff --git a/migrations/versions/26-account_types.py b/migrations/versions/26-account_types.py index f5b970becd..e9aa607afd 100644 --- a/migrations/versions/26-account_types.py +++ b/migrations/versions/26-account_types.py @@ -211,7 +211,7 @@ def upgrade() -> None: } module_awareness = ModuleVisibilityAwareness( - roots={group_visibility.root for group_visibility in group_visibilities}, + roots=list({group_visibility.root for group_visibility in group_visibilities}), ) conn.execute( From da6b5edd9e9deb917c8a872b8def99e5575b99f8 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:15:58 +0200 Subject: [PATCH 24/28] Ignore ty known issue --- app/core/payment/payment_tool.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/core/payment/payment_tool.py b/app/core/payment/payment_tool.py index b7bb7efa10..55f6b59ad0 100644 --- a/app/core/payment/payment_tool.py +++ b/app/core/payment/payment_tool.py @@ -180,19 +180,19 @@ async def init_checkout( secret = security.generate_token(nbytes=12) init_checkout_body = HelloAssoApiV5ModelsCartsInitCheckoutBody( - total_amount=checkout_amount, - initial_amount=checkout_amount, - item_name=checkout_name, - back_url=redirection_uri, - error_url=redirection_uri, - return_url=redirection_uri, - contains_donation=False, + total_amount=checkout_amount, # ty:ignore[unknown-argument] + initial_amount=checkout_amount, # ty:ignore[unknown-argument] + item_name=checkout_name, # ty:ignore[unknown-argument] + back_url=redirection_uri, # ty:ignore[unknown-argument] + error_url=redirection_uri, # ty:ignore[unknown-argument] + return_url=redirection_uri, # ty:ignore[unknown-argument] + contains_donation=False, # ty:ignore[unknown-argument] payer=payer, metadata=schemas_payment.HelloAssoCheckoutMetadata( secret=secret, hyperion_checkout_id=str(checkout_model_id), ).model_dump(), - ) + ) # ty:ignore[missing-argument] # See https://github.com/astral-sh/ty/issues/1438 response: HelloAssoApiV5ModelsCartsInitCheckoutResponse with ApiClient(configuration) as api_client: From 201da422844766141c9e0ba30a62cec8a4507493 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:16:50 +0200 Subject: [PATCH 25/28] Make some column non nullable in Seed to fix inconsistency with schemas --- .../seed_library/models_seed_library.py | 8 +- migrations/versions/59-seed_non_optionals.py | 177 ++++++++++++++++++ 2 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/59-seed_non_optionals.py diff --git a/app/modules/seed_library/models_seed_library.py b/app/modules/seed_library/models_seed_library.py index 96e3e5ad8f..aa222b3bdf 100644 --- a/app/modules/seed_library/models_seed_library.py +++ b/app/modules/seed_library/models_seed_library.py @@ -17,10 +17,10 @@ class Species(Base): id: Mapped[PrimaryKey] prefix: Mapped[str] = mapped_column(unique=True) # 3 letters name: Mapped[str] = mapped_column(unique=True) - difficulty: Mapped[int | None] + difficulty: Mapped[int] card: Mapped[str | None] nb_seeds_recommended: Mapped[int | None] - species_type: Mapped[SpeciesType | None] + species_type: Mapped[SpeciesType] start_season: Mapped[date | None] end_season: Mapped[date | None] time_maturation: Mapped[int | None] # number of days @@ -35,7 +35,7 @@ class Plant(Base): ForeignKey("seed_library_species.id"), ) propagation_method: Mapped[PropagationMethod] - nb_seeds_envelope: Mapped[int | None] + nb_seeds_envelope: Mapped[int] ancestor_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("seed_library_plants.id"), ) @@ -45,7 +45,7 @@ class Plant(Base): ForeignKey("core_user.id"), index=True, ) - confidential: Mapped[bool | None] + confidential: Mapped[bool] nickname: Mapped[str | None] planting_date: Mapped[date | None] borrowing_date: Mapped[date | None] diff --git a/migrations/versions/59-seed_non_optionals.py b/migrations/versions/59-seed_non_optionals.py new file mode 100644 index 0000000000..7457eaf26b --- /dev/null +++ b/migrations/versions/59-seed_non_optionals.py @@ -0,0 +1,177 @@ +"""empty message + +Create Date: 2026-04-13 11:44:05.698432 +""" + +import uuid +from collections.abc import Sequence +from typing import TYPE_CHECKING + +from sqlalchemy.dialects import postgresql + +if TYPE_CHECKING: + from pytest_alembic import MigrationContext + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "3108c3bc5425" +down_revision: str | None = "e58ffcd6b9eb" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.execute( + """ + UPDATE seed_library_plants + SET nb_seeds_envelope = 0 + WHERE nb_seeds_envelope IS NULL + """, + ) + op.alter_column( + "seed_library_plants", + "nb_seeds_envelope", + existing_type=sa.INTEGER(), + nullable=False, + ) + op.execute( + """ + UPDATE seed_library_plants + SET confidential = FALSE + WHERE confidential IS NULL + """, + ) + op.alter_column( + "seed_library_plants", + "confidential", + existing_type=sa.BOOLEAN(), + nullable=False, + ) + op.execute( + """ + UPDATE seed_library_species + SET difficulty = 0 + WHERE difficulty IS NULL + """, + ) + op.alter_column( + "seed_library_species", + "difficulty", + existing_type=sa.INTEGER(), + nullable=False, + ) + op.execute( + """ + UPDATE seed_library_species + SET species_type = 'other' + WHERE species_type IS NULL + """, + ) + op.alter_column( + "seed_library_species", + "species_type", + existing_type=postgresql.ENUM( + "aromatic", + "vegetables", + "interior", + "fruit", + "cactus", + "ornamental", + "succulent", + "other", + name="speciestype", + ), + nullable=False, + ) + + +def downgrade() -> None: + op.alter_column( + "seed_library_species", + "species_type", + existing_type=postgresql.ENUM( + "aromatic", + "vegetables", + "interior", + "fruit", + "cactus", + "ornamental", + "succulent", + "other", + name="speciestype", + ), + nullable=True, + ) + op.alter_column( + "seed_library_species", + "difficulty", + existing_type=sa.INTEGER(), + nullable=True, + ) + op.alter_column( + "seed_library_plants", + "confidential", + existing_type=sa.BOOLEAN(), + nullable=True, + ) + op.alter_column( + "seed_library_plants", + "nb_seeds_envelope", + existing_type=sa.INTEGER(), + nullable=True, + ) + + +def pre_test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + species_id = uuid.uuid4() + plant_id = uuid.uuid4() + + # Insert species with NULL values (future NOT NULL fields) + alembic_runner.insert_into( + "seed_library_species", + { + "id": species_id, + "prefix": "TST", + "name": f"Test species {species_id}", + "difficulty": None, # will be backfilled to 0 + "card": None, + "nb_seeds_recommended": None, + "species_type": None, # will be backfilled to 'other' + "start_season": None, + "end_season": None, + "time_maturation": None, + }, + ) + + # Insert plant with NULL values (future NOT NULL fields) + alembic_runner.insert_into( + "seed_library_plants", + { + "id": plant_id, + "reference": f"REF-{plant_id}", + "state": "waiting", # adjust if enum differs + "species_id": species_id, + "propagation_method": "seed", # adjust if enum differs + "nb_seeds_envelope": None, # will be backfilled to 0 + "ancestor_id": None, + "previous_note": None, + "current_note": None, + "borrower_id": None, + "confidential": None, # will be backfilled to False + "nickname": None, + "planting_date": None, + "borrowing_date": None, + }, + ) + + +def test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass From a26518d6b43bc10c5c440792a36e11d7d1ce7d38 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:17:01 +0200 Subject: [PATCH 26/28] Run Ty in CI --- .github/workflows/lintandformat.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/lintandformat.yml b/.github/workflows/lintandformat.yml index 05e36a8c53..34f73e81ba 100644 --- a/.github/workflows/lintandformat.yml +++ b/.github/workflows/lintandformat.yml @@ -46,6 +46,9 @@ jobs: ruff check --output-format=github ruff format --check + - name: Type checking using Ty + run: ty check --output-format=github --error=all + - name: Cache .mypy_cache folder id: mypy_cache uses: actions/cache@v4.3.0 From e2f95e86905aa3406fc59be738383b78e1c90b20 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:36:38 +0200 Subject: [PATCH 27/28] Fix warnings --- app/core/mypayment/endpoints_mypayment.py | 1 + app/core/payment/payment_tool.py | 34 ++++++++++--------- app/modules/cdr/utils_cdr.py | 1 + app/modules/loan/endpoints_loan.py | 30 +++++++--------- .../seed_library/endpoints_seed_library.py | 14 ++++---- .../endpoints_sport_competition.py | 5 ++- app/utils/communication/notifications.py | 2 +- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/core/mypayment/endpoints_mypayment.py b/app/core/mypayment/endpoints_mypayment.py index bac104f0d5..285d19a4c1 100644 --- a/app/core/mypayment/endpoints_mypayment.py +++ b/app/core/mypayment/endpoints_mypayment.py @@ -2683,6 +2683,7 @@ async def refund_transaction( ) if wallet_previously_credited.user is not None: + wallet_previously_debited_name: str = "Unknown" if wallet_previously_debited.user is not None: wallet_previously_debited_name = wallet_previously_debited.user.full_name elif wallet_previously_debited.store is not None: diff --git a/app/core/payment/payment_tool.py b/app/core/payment/payment_tool.py index 55f6b59ad0..4ec012994b 100644 --- a/app/core/payment/payment_tool.py +++ b/app/core/payment/payment_tool.py @@ -210,23 +210,25 @@ async def init_checkout( hyperion_error_logger.exception( f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name} (no payer info provided).", ) - else: - payer_user_name = f"{payer_user.firstname} {payer_user.name}" - hyperion_error_logger.warning( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. Retrying without payer infos for {payer_user_name}", - ) + raise + + payer_user_name = f"{payer_user.firstname} {payer_user.name}" + hyperion_error_logger.warning( + f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}. Retrying without payer infos for {payer_user_name}", + ) - init_checkout_body.payer = None - try: - response = checkout_api.organizations_organization_slug_checkout_intents_post( - self._helloasso_slug, - init_checkout_body, - ) - except UnauthorizedException: - # HelloAsso returned a 401 unauthorized again - hyperion_error_logger.exception( - f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos", - ) + init_checkout_body.payer = None + try: + response = checkout_api.organizations_organization_slug_checkout_intents_post( + self._helloasso_slug, + init_checkout_body, + ) + except UnauthorizedException: + # HelloAsso returned a 401 unauthorized again + hyperion_error_logger.exception( + f"Payment: failed to init a checkout with HA for module {module} and name {checkout_name}, with and without payer {payer_user_name} infos", + ) + raise if response and response.id and response.redirect_url: checkout_model = models_payment.Checkout( diff --git a/app/modules/cdr/utils_cdr.py b/app/modules/cdr/utils_cdr.py index a29a781f88..129e26b5d4 100644 --- a/app/modules/cdr/utils_cdr.py +++ b/app/modules/cdr/utils_cdr.py @@ -437,6 +437,7 @@ def write_product_headers( custom_cols = prod_struct["custom_cols"] needs_validation = prod_struct["needs_validation"] + end_col = 0 if variants_info: start_col = variants_info[0]["qty_col"] end_col = ( diff --git a/app/modules/loan/endpoints_loan.py b/app/modules/loan/endpoints_loan.py index 79158c3051..c364ee5a77 100644 --- a/app/modules/loan/endpoints_loan.py +++ b/app/modules/loan/endpoints_loan.py @@ -758,6 +758,8 @@ async def update_loan( detail="Invalid user_id", ) + items: list[tuple[models_loan.Item, int]] = [] + # If a new list of items was provided, we need to mark old items as available and new items as not available if loan_update.items_borrowed: for old_item in loan.items: @@ -775,8 +777,6 @@ async def update_loan( # We remove the old items from the database await cruds_loan.delete_loan_content_by_loan_id(loan_id=loan_id, db=db) - items: list[tuple[models_loan.Item, int]] = [] - # All items should be valid, available and belong to the loaner for item_borrowed in loan_update.items_borrowed: item_id: str = item_borrowed.item_id @@ -816,14 +816,11 @@ async def update_loan( # We make a list of every new item with the quantity borrowed to update the loaned quantity and create the loaned content items.append((item, quantity)) - try: - await cruds_loan.update_loan( - loan_id=loan_id, - loan_update=loan_update, - db=db, - ) - except ValueError as error: - raise HTTPException(status_code=422, detail=str(error)) + await cruds_loan.update_loan( + loan_id=loan_id, + loan_update=loan_update, + db=db, + ) for item, quantity in items: # We add each item to the loan @@ -979,7 +976,7 @@ async def extend_loan( status_code=400, detail="Invalid loan_id", ) - end = loan.end + # The user should be a member of the loaner's manager group if not is_user_member_of_any_group(user, [loan.loaner.group_manager_id]): raise HTTPException( @@ -987,16 +984,15 @@ async def extend_loan( detail=f"Unauthorized to manage {loan.loaner_id} loaner", ) + end = loan.end if loan_extend.end is not None: end = loan_extend.end - loan_update = schemas_loan.LoanUpdate( - end=end, - ) elif loan_extend.duration is not None: end = loan.end + timedelta(seconds=loan_extend.duration) - loan_update = schemas_loan.LoanUpdate( - end=end, - ) + + loan_update = schemas_loan.LoanUpdate( + end=end, + ) await cruds_loan.update_loan( loan_id=loan_id, diff --git a/app/modules/seed_library/endpoints_seed_library.py b/app/modules/seed_library/endpoints_seed_library.py index e96a258c80..9f712df8b2 100644 --- a/app/modules/seed_library/endpoints_seed_library.py +++ b/app/modules/seed_library/endpoints_seed_library.py @@ -331,13 +331,13 @@ async def create_plant( "Species not found", ) date = datetime.now(tz=UTC) - if species_reference: - reference = f"{species_reference.prefix}-{date.day:02}-{date.month:02}-{str(date.year)[2:]}-" - plant_number = await cruds_seed_library.count_plants_created_today( - reference, - db, - ) - reference = f"{species_reference.prefix}-{date.day:02}-{date.month:02}-{str(date.year)[2:]}-{plant_number:03}" + + reference = f"{species_reference.prefix}-{date.day:02}-{date.month:02}-{str(date.year)[2:]}-" + plant_number = await cruds_seed_library.count_plants_created_today( + reference, + db, + ) + reference = f"{species_reference.prefix}-{date.day:02}-{date.month:02}-{str(date.year)[2:]}-{plant_number:03}" plant = schemas_seed_library.PlantComplete( id=uuid.uuid4(), diff --git a/app/modules/sport_competition/endpoints_sport_competition.py b/app/modules/sport_competition/endpoints_sport_competition.py index 72b5f0be0f..424c0c7196 100644 --- a/app/modules/sport_competition/endpoints_sport_competition.py +++ b/app/modules/sport_competition/endpoints_sport_competition.py @@ -2376,6 +2376,7 @@ async def join_sport( status_code=400, detail="Maximum number of substitutes in the team reached", ) + team_id = participant_info.team_id elif participant_info.team_id is not None: raise HTTPException( @@ -2394,6 +2395,8 @@ async def join_sport( ) await cruds_sport_competition.add_team(new_team, db) + team_id = new_team.id + participant = schemas_sport_competition.Participant( user_id=user.user_id, sport_id=sport_id, @@ -2402,7 +2405,7 @@ async def join_sport( license=participant_info.license, substitute=participant_info.substitute, is_license_valid=False, - team_id=participant_info.team_id or new_team.id, + team_id=team_id, ) await cruds_sport_competition.add_participant( participant, diff --git a/app/utils/communication/notifications.py b/app/utils/communication/notifications.py index bb9c2828d2..b104f2ac77 100644 --- a/app/utils/communication/notifications.py +++ b/app/utils/communication/notifications.py @@ -163,8 +163,8 @@ def _send_firebase_push_notification_by_topic( if not self.use_firebase: return + topic = str(topic_id) try: - topic = str(topic_id) message = messaging.Message( topic=topic, data={"action_module": message_content.action_module}, From c0acdc6e5d990423f755aad77b91c17d56ae6c2c Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:11:55 +0200 Subject: [PATCH 28/28] Fix thick_columns for Sport Competition --- .../sport_competition/utils/data_exporter/global_exporter.py | 1 + .../utils/data_exporter/school_participants_exporter.py | 1 + 2 files changed, 2 insertions(+) diff --git a/app/modules/sport_competition/utils/data_exporter/global_exporter.py b/app/modules/sport_competition/utils/data_exporter/global_exporter.py index fd087106c5..86681cce5c 100644 --- a/app/modules/sport_competition/utils/data_exporter/global_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/global_exporter.py @@ -62,6 +62,7 @@ def build_data_rows( col_idx: int, ) -> tuple[list[list[str | int]], list[int]]: data_rows: list[list[str | int]] = [] + thick_columns = [len(FIXED_COLUMNS) - 1] for user in users: user_purchases = users_purchases.get(user.user.id, []) row: list[str | int] = [""] * col_idx diff --git a/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py b/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py index b9700b992f..2d26105f7a 100644 --- a/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/school_participants_exporter.py @@ -72,6 +72,7 @@ def build_data_rows( col_idx: int, ) -> tuple[list[list[str | int]], list[int]]: data_rows: list[list[str | int]] = [] + thick_columns = [len(FIXED_COLUMNS) - 1] for user in users: user_purchases = users_purchases.get(user.user.id, []) row: list[str | int] = [""] * col_idx