From b574a819f2bc9557c569d22a682c5a5c9200db1b Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:17:40 +0200 Subject: [PATCH 01/15] Permissions factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Timothée Robert <114694873+Rotheem@users.noreply.github.com> --- app/core/permissions/endpoints_permissions.py | 3 +- app/core/permissions/factory_permissions.py | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/core/permissions/factory_permissions.py diff --git a/app/core/permissions/endpoints_permissions.py b/app/core/permissions/endpoints_permissions.py index 913b80902b..cbf7ec7265 100644 --- a/app/core/permissions/endpoints_permissions.py +++ b/app/core/permissions/endpoints_permissions.py @@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.permissions import cruds_permissions, schemas_permissions +from app.core.permissions.factory_permissions import CorePermissionsFactory from app.dependencies import ( get_db, is_user, @@ -29,7 +30,7 @@ root="permissions", tag="Permissions", router=router, - factory=None, + factory=CorePermissionsFactory(), ) hyperion_security_logger = logging.getLogger("hyperion.security") diff --git a/app/core/permissions/factory_permissions.py b/app/core/permissions/factory_permissions.py new file mode 100644 index 0000000000..6617c6df08 --- /dev/null +++ b/app/core/permissions/factory_permissions.py @@ -0,0 +1,29 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.groups.groups_type import GroupType +from app.core.permissions import cruds_permissions, schemas_permissions +from app.core.utils.config import Settings +from app.module import permissions_list +from app.types.factory import Factory + + +class CorePermissionsFactory(Factory): + @classmethod + async def run(cls, db: AsyncSession, settings: Settings) -> None: + for permission in permissions_list: + await cruds_permissions.create_group_permission( + permission=schemas_permissions.CoreGroupPermission( + permission_name=permission, + group_id=GroupType.admin.value, + ), + db=db, + ) + await db.commit() + + @classmethod + async def should_run(cls, db: AsyncSession): + permissions = await cruds_permissions.get_permissions( + permissions_list, + db, + ) + return len(permissions) == 0 From 8d1d3953d1276fa03fd1efe08f988af990095fe5 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:18:00 +0200 Subject: [PATCH 02/15] Use new custom_generate_unique_id api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Timothée Robert <114694873+Rotheem@users.noreply.github.com> --- app/app.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/app.py b/app/app.py index de892b91c4..a8f083aecb 100644 --- a/app/app.py +++ b/app/app.py @@ -400,21 +400,21 @@ async def initialize_notification_topics( ) -def use_route_path_as_operation_ids(app: FastAPI) -> None: +def use_route_path_as_operation_id(route: APIRoute) -> str: """ - Simplify operation IDs so that generated API clients have simpler function names. + Simplify operation ID so that generated API clients have simpler function names. Theses names may be used by API clients to generate function names. The operation_id will have the format "method_path", like "get_users_me". See https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/ """ - for route in app.routes: - if isinstance(route, APIRoute): - # The operation_id should be unique. - # It is possible to set multiple methods for the same endpoint method but it's not considered a good practice. - method = "_".join(route.methods) - route.operation_id = method.lower() + route.path.replace("/", "_") + if route.methods: + # The operation_id should be unique. + # It is possible to set multiple methods for the same endpoint method but it's not considered a good practice. + method = "_".join(route.methods) + return method.lower() + route.path.replace("/", "_") + return route.name def init_db( @@ -645,9 +645,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState]: title="Hyperion", version=settings.HYPERION_VERSION, lifespan=lifespan, + custom_generate_unique_id=use_route_path_as_operation_id, ) app.include_router(api.api_router) - use_route_path_as_operation_ids(app) app.add_middleware( CORSMiddleware, From c604c0c1b9afcc5ebbdc7dda17144f4820c209ad Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:21:21 +0200 Subject: [PATCH 03/15] Ignore "PLR0917, Too many positional arguments" --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 915d16c5a4..30bbeb3de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ ignore = [ "TC002", # "Move into type-checking block" "TC003", # "Move into type-checking block" "UP037", # Remove quotes from type annotations + "PLR0917", # "Too many positional arguments" ] # Allow fix for all enabled rules (when `--fix`) is provided. From 67af2a360db0d9a7840663a1273521e8f7f3cac5 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:24:13 +0200 Subject: [PATCH 04/15] Ty 0.0.63 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 17435ee4ed..fbd1abf7f1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,9 +9,9 @@ pytest-cov==7.1.0 pytest-mock==3.15.1 pytest==9.0.3 ruff==0.15.16 -ty==0.0.44 types-Authlib==1.6.11.20260518 types-fpdf2==2.8.4.20260518 +ty==0.0.63 types-psutil==7.2.2.20260518 types-redis==4.6.0.20241004 types-requests==2.33.0.20260518 \ No newline at end of file From bcb0f4c84c6f3b1f8abd5286c8670f726fe209de Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:24:27 +0200 Subject: [PATCH 05/15] Remove ty:ignore[unknown-argument] --- app/core/checkout/payment_tool.py | 22 +++++++++++----------- tests/core/test_checkout.py | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/core/checkout/payment_tool.py b/app/core/checkout/payment_tool.py index 4daa04f8f6..e0ddc5a5b7 100644 --- a/app/core/checkout/payment_tool.py +++ b/app/core/checkout/payment_tool.py @@ -166,10 +166,10 @@ async def init_checkout( payer: HelloAssoApiV5ModelsCartsCheckoutPayer | None = None if payer_user is not None: payer = HelloAssoApiV5ModelsCartsCheckoutPayer( - first_name=payer_user.firstname, # ty:ignore[unknown-argument] - last_name=payer_user.name, # ty:ignore[unknown-argument] + first_name=payer_user.firstname, + last_name=payer_user.name, email=payer_user.email, - date_of_birth=datetime.combine( # ty:ignore[unknown-argument] + date_of_birth=datetime.combine( payer_user.birthday, datetime.min.time(), tzinfo=UTC, @@ -182,19 +182,19 @@ async def init_checkout( secret = security.generate_token(nbytes=12) init_checkout_body = HelloAssoApiV5ModelsCartsInitCheckoutBody( - 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] + 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, payer=payer, metadata=schemas_checkout.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: diff --git a/tests/core/test_checkout.py b/tests/core/test_checkout.py index 201460c0ff..5b0922a819 100644 --- a/tests/core/test_checkout.py +++ b/tests/core/test_checkout.py @@ -440,7 +440,7 @@ async def test_payment_tool_init_checkout( mock_checkout_api = mocker.MagicMock() mock_checkout_api.organizations_organization_slug_checkout_intents_post.return_value = HelloAssoApiV5ModelsCartsInitCheckoutResponse( id=7, - redirect_url=redirect_url, # ty:ignore[unknown-argument] + redirect_url=redirect_url, ) mocker.patch( "app.core.checkout.payment_tool.CheckoutApi", @@ -502,7 +502,7 @@ def init_a_checkout_side_effect( raise UnauthorizedException return HelloAssoApiV5ModelsCartsInitCheckoutResponse( id=7, - redirect_url=redirect_url, # ty:ignore[unknown-argument] + redirect_url=redirect_url, ) mocker.patch.object( From da2828f7815ca6e74768944cad79d109823f88cf Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:41:49 +0200 Subject: [PATCH 06/15] Fix types --- app/app.py | 4 +-- app/core/utils/config.py | 6 ++--- app/dependencies.py | 2 +- app/modules/amap/endpoints_amap.py | 6 ++--- app/modules/cdr/utils_cdr.py | 16 ++++++------ app/modules/raffle/endpoints_raffle.py | 4 +-- .../recommendation/cruds_recommendation.py | 6 ++--- .../utils/data_exporter/captain_exporter.py | 8 +++--- .../utils/data_exporter/commons.py | 4 +-- .../utils/data_exporter/global_exporter.py | 20 +++++++------- .../school_participants_exporter.py | 20 +++++++------- .../data_exporter/school_quotas_exporter.py | 26 +++++++++---------- .../sport_participants_exporter.py | 6 ++--- .../data_exporter/sport_quotas_exporter.py | 8 +++--- app/types/scheduler.py | 4 +-- app/types/sqlalchemy.py | 2 +- app/types/websocket.py | 2 +- app/utils/initialization.py | 2 +- app/utils/loggers_tools/matrix_handler.py | 8 +++--- app/utils/loggers_tools/s3_handler.py | 8 +++--- app/utils/redis.py | 6 ++--- app/utils/state.py | 8 +++--- migrations/versions/21-fix_phonebook.py | 2 +- 23 files changed, 89 insertions(+), 89 deletions(-) diff --git a/app/app.py b/app/app.py index a8f083aecb..306b7c1ca0 100644 --- a/app/app.py +++ b/app/app.py @@ -523,7 +523,7 @@ async def init_lifespan( hyperion_error_logger=hyperion_error_logger, ) - redis_client: Redis | None = app.dependency_overrides.get( + redis_client: Redis[bytes] | None = app.dependency_overrides.get( get_redis_client, get_redis_client, )() @@ -697,7 +697,7 @@ async def logging_middleware( port = request.client.port client_address = f"{ip_address}:{port}" - redis_client: Redis | None = get_redis_client_dependency() + redis_client: Redis[bytes] | None = get_redis_client_dependency() # We test the ip address with the redis limiter process = True diff --git a/app/core/utils/config.py b/app/core/utils/config.py index 6f65a16f2d..31dd64d88e 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -65,9 +65,9 @@ class School(BaseModel): # On registration, user whose email match these regex will be automatically assigned to the corresponding account type # Use simple quotes to avoid escaping the regex # Ex: `student_email_regex: '^[\w\-.]*@domain.fr$'` - student_email_regex: Pattern - staff_email_regex: Pattern | None = None - former_student_email_regex: Pattern | None = None + student_email_regex: Pattern[str] + staff_email_regex: Pattern[str] | None = None + former_student_email_regex: Pattern[str] | None = None # If event should be confirmed by a moderator before being added to the calendar require_event_confirmation: bool = True diff --git a/app/dependencies.py b/app/dependencies.py index 04533ef8de..297b287e3e 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -239,7 +239,7 @@ async def get_unsafe_db() -> AsyncGenerator[AsyncSession]: yield db -def get_redis_client() -> redis.Redis | None: +def get_redis_client() -> redis.Redis[bytes] | None: """ Dependency that returns the redis client diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index ca3259d43d..fb10ae127a 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -458,7 +458,7 @@ async def get_order_by_id( async def add_order_to_delievery( order: schemas_amap.OrderBase, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -611,7 +611,7 @@ async def edit_order_from_delivery( order_id: str, order: schemas_amap.OrderEdit, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -737,7 +737,7 @@ async def edit_order_from_delivery( async def remove_order( order_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), diff --git a/app/modules/cdr/utils_cdr.py b/app/modules/cdr/utils_cdr.py index b0cc5ead24..2136fb2ac9 100644 --- a/app/modules/cdr/utils_cdr.py +++ b/app/modules/cdr/utils_cdr.py @@ -370,7 +370,7 @@ def build_data_rows( users: list[models_users.CoreUser], users_purchases: dict[str, list[models_cdr.Purchase]], users_answers: dict[str, list[models_cdr.CustomData]], - product_structure: dict, + product_structure: dict, # ty:ignore[missing-type-argument] col_idx: int, ): data_rows = [] @@ -413,7 +413,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, fixed_columns: list[str], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col, title in enumerate(fixed_columns): worksheet.merge_range(0, col, 2, col, title, formats["header"]["base"]) @@ -421,9 +421,9 @@ def write_fixed_headers( def write_product_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - product_structure: dict, + product_structure: dict, # ty:ignore[missing-type-argument] fixed_columns: list[str], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] max_lens: list[int], ): product_end_cols = [ @@ -542,10 +542,10 @@ def write_product_headers( def write_data_rows( worksheet: xlsxwriter.Workbook.worksheet_class, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] product_end_cols: list[int], variant_end_cols: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] max_lens: list[int], start_row: int = 3, ): @@ -600,9 +600,9 @@ def write_to_excel( worksheet_name: str, fixed_columns: list[str], product_structure, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] col_idx: int, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet(worksheet_name) max_lens = [len(c) for c in fixed_columns] + [0] * (col_idx - len(fixed_columns)) diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 1fa4ca7a0f..4fe4b76ca5 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -480,7 +480,7 @@ async def get_tickets( async def buy_ticket( pack_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.access_raffle]), ), @@ -981,7 +981,7 @@ async def edit_cash_by_id( user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.manage_cash]), ), - redis_client: Redis = Depends(get_redis_client), + redis_client: Redis[bytes] = Depends(get_redis_client), ): """ Edit cash for an user. This will add the balance to the current balance. diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 23bce676c0..6fce5059a9 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -1,6 +1,6 @@ import uuid from collections.abc import Sequence -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -40,7 +40,7 @@ async def update_recommendation( .where(models_recommendation.Recommendation.id == recommendation_id) .values(**recommendation.model_dump(exclude_none=True)), ) - if cast("CursorResult", result).rowcount == 1: + if cast("CursorResult[Any]", result).rowcount == 1: await db.flush() else: await db.rollback() @@ -56,7 +56,7 @@ async def delete_recommendation( models_recommendation.Recommendation.id == recommendation_id, ), ) - if cast("CursorResult", result).rowcount == 1: + if cast("CursorResult[Any]", result).rowcount == 1: await db.flush() else: await db.rollback() diff --git a/app/modules/sport_competition/utils/data_exporter/captain_exporter.py b/app/modules/sport_competition/utils/data_exporter/captain_exporter.py index e14b6a7d4c..40025f958b 100644 --- a/app/modules/sport_competition/utils/data_exporter/captain_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/captain_exporter.py @@ -28,7 +28,7 @@ def build_data_rows( captains: list[schemas_sport_competition.ParticipantComplete], sports: list[schemas_sport_competition.Sport], -) -> tuple[list[list], list[int]]: +) -> tuple[list[list], list[int]]: # ty:ignore[missing-type-argument] data_rows = [] thick_columns = [len(FIXED_COLUMNS) - 1] @@ -62,7 +62,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col, title in enumerate(FIXED_COLUMNS): worksheet.merge_range(0, col, 1, col, title, formats["header"]["base"]) @@ -70,9 +70,9 @@ def write_fixed_headers( def write_to_excel( workbook: xlsxwriter.Workbook, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet("Données") columns_max_length = [len(c) for c in FIXED_COLUMNS] diff --git a/app/modules/sport_competition/utils/data_exporter/commons.py b/app/modules/sport_competition/utils/data_exporter/commons.py index 5d58f05175..f3216ca903 100644 --- a/app/modules/sport_competition/utils/data_exporter/commons.py +++ b/app/modules/sport_competition/utils/data_exporter/commons.py @@ -144,9 +144,9 @@ def make_format( def write_data_rows( worksheet: xlsxwriter.Workbook.worksheet_class, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] columns_max_length: list[int], start_row: int = 5, ): 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 5c25bc7bdf..469083e525 100644 --- a/app/modules/sport_competition/utils/data_exporter/global_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/global_exporter.py @@ -62,7 +62,7 @@ def build_data_rows( users_participant: dict[str, schemas_sport_competition.ParticipantComplete] | None, users_purchases: dict[str, list[schemas_sport_competition.PurchaseComplete]], users_payments: dict[str, list[schemas_sport_competition.PaymentComplete]] | None, - product_structure: tuple[list, int] | None, + product_structure: tuple[list, int] | None, # ty:ignore[missing-type-argument] col_idx: int, ) -> tuple[list[list[str | int]], list[int]]: data_rows: list[list[str | int]] = [] @@ -146,7 +146,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet.merge_range( 0, @@ -162,7 +162,7 @@ def write_fixed_headers( def write_participant_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] columns_max_length: list[int], ): worksheet.merge_range( @@ -180,7 +180,7 @@ def write_participant_headers( def write_payment_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] start_index: int, columns_max_length: list[int], ): @@ -199,8 +199,8 @@ def write_payment_headers( def write_product_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - product_structure: tuple[list, int], - formats: dict, + product_structure: tuple[list, int], # ty:ignore[missing-type-argument] + formats: dict, # ty:ignore[missing-type-argument] start_index: int, columns_max_length: list[int], ) -> tuple[list[int], list[int]]: @@ -286,11 +286,11 @@ def write_product_headers( def write_to_excel( parameters: list[ExcelExportParams], workbook: xlsxwriter.Workbook, - product_structure: tuple[list, int] | None, - data_rows: list, + product_structure: tuple[list, int] | None, # ty:ignore[missing-type-argument] + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], col_idx: int, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet("Données") columns_max_length = [len(c) for c in FIXED_COLUMNS] + [0] * ( @@ -369,7 +369,7 @@ def construct_users_excel_with_parameters( ), ) - product_structure: tuple[list, int] | None = None + product_structure: tuple[list, int] | None = None # ty:ignore[missing-type-argument] col_idx = len(FIXED_COLUMNS) if ExcelExportParams.purchases in parameters and products is not None: products.sort( 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 2140a52048..72ad006af2 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,7 +72,7 @@ def build_data_rows( users_participant: dict[str, schemas_sport_competition.ParticipantComplete] | None, users_purchases: dict[str, list[schemas_sport_competition.PurchaseComplete]], users_payments: dict[str, list[schemas_sport_competition.PaymentComplete]] | None, - product_structure: tuple[list, int] | None, + product_structure: tuple[list, int] | None, # ty:ignore[missing-type-argument] col_idx: int, ) -> tuple[list[list[str | int]], list[int]]: data_rows: list[list[str | int]] = [] @@ -159,7 +159,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet.merge_range( 0, @@ -175,7 +175,7 @@ def write_fixed_headers( def write_participant_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] columns_max_length: list[int], ): worksheet.merge_range( @@ -193,7 +193,7 @@ def write_participant_headers( def write_payment_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] start_index: int, columns_max_length: list[int], ): @@ -212,8 +212,8 @@ def write_payment_headers( def write_product_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - product_structure: tuple[list, int], - formats: dict, + product_structure: tuple[list, int], # ty:ignore[missing-type-argument] + formats: dict, # ty:ignore[missing-type-argument] start_index: int, columns_max_length: list[int], ) -> tuple[list[int], list[int]]: @@ -299,11 +299,11 @@ def write_product_headers( def write_to_excel( parameters: list[ExcelExportParams], workbook: xlsxwriter.Workbook, - product_structure: tuple[list, int] | None, - data_rows: list, + product_structure: tuple[list, int] | None, # ty:ignore[missing-type-argument] + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], col_idx: int, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet("Données") columns_max_length = [len(c) for c in FIXED_COLUMNS] + [0] * ( @@ -377,7 +377,7 @@ def construct_school_users_excel_with_parameters( u.user.firstname.lower(), ), ) - product_structure: tuple[list, int] | None = None + product_structure: tuple[list, int] | None = None # ty:ignore[missing-type-argument] col_idx = len(FIXED_COLUMNS) if ExcelExportParams.purchases in parameters and products is not None: products.sort( diff --git a/app/modules/sport_competition/utils/data_exporter/school_quotas_exporter.py b/app/modules/sport_competition/utils/data_exporter/school_quotas_exporter.py index c6fff82bc2..3c2aa5cb0d 100644 --- a/app/modules/sport_competition/utils/data_exporter/school_quotas_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/school_quotas_exporter.py @@ -31,8 +31,8 @@ def build_data_rows( school_product_quotas: list[schemas_sport_competition.SchoolProductQuota], sports: list[schemas_sport_competition.Sport], products: list[schemas_sport_competition.ProductComplete], -) -> tuple[list[list], list[list[int]]]: - data_rows: list[list] = [[], [], []] +) -> tuple[list[list], list[list[int]]]: # ty:ignore[missing-type-argument] + data_rows: list[list] = [[], [], []] # ty:ignore[missing-type-argument] thick_columns: list[list[int]] = [[], [], []] # First column index sport_dict = {sport.id: sport for sport in sports} @@ -102,7 +102,7 @@ def build_data_rows( def write_sports_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col_idx, column in enumerate(SPORT_COLUMNS): worksheet.write(0, col_idx, column, formats["header"]["base"]) @@ -110,7 +110,7 @@ def write_sports_headers( def write_generals_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col_idx, column in enumerate(GENERAL_COLUMNS): worksheet.write(0, col_idx, column, formats["header"]["base"]) @@ -118,7 +118,7 @@ def write_generals_headers( def write_products_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col_idx, column in enumerate(PRODUCT_COLUMNS): worksheet.write(0, col_idx, column, formats["header"]["base"]) @@ -126,9 +126,9 @@ def write_products_headers( def write_general_quota_sheet( worksheet: xlsxwriter.Workbook.worksheet_class, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): write_generals_headers(worksheet, formats) columns_max_length = [len(c) for c in SPORT_COLUMNS] @@ -146,9 +146,9 @@ def write_general_quota_sheet( def write_sport_quota_sheet( worksheet: xlsxwriter.Workbook.worksheet_class, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): write_sports_headers(worksheet, formats) columns_max_length = [len(c) for c in SPORT_COLUMNS] @@ -166,9 +166,9 @@ def write_sport_quota_sheet( def write_product_quota_sheet( worksheet: xlsxwriter.Workbook.worksheet_class, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): write_products_headers(worksheet, formats) columns_max_length = [len(c) for c in SPORT_COLUMNS] @@ -186,9 +186,9 @@ def write_product_quota_sheet( def write_to_excel( workbook: xlsxwriter.Workbook, - data_rows: list[list], + data_rows: list[list], # ty:ignore[missing-type-argument] thick_columns: list[list[int]], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): if len(data_rows[0]) > 0: general_worksheet = workbook.add_worksheet("Quotas généraux") diff --git a/app/modules/sport_competition/utils/data_exporter/sport_participants_exporter.py b/app/modules/sport_competition/utils/data_exporter/sport_participants_exporter.py index 3148bcc746..e1db3ec39e 100644 --- a/app/modules/sport_competition/utils/data_exporter/sport_participants_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/sport_participants_exporter.py @@ -71,7 +71,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col, title in enumerate(FIXED_COLUMNS): worksheet.write(1, col, title, formats["header"]["base"]) @@ -79,9 +79,9 @@ def write_fixed_headers( def write_to_excel( workbook: xlsxwriter.Workbook, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet("Données") columns_max_length = [len(c) for c in FIXED_COLUMNS] diff --git a/app/modules/sport_competition/utils/data_exporter/sport_quotas_exporter.py b/app/modules/sport_competition/utils/data_exporter/sport_quotas_exporter.py index a708e7d5f8..5c0013385b 100644 --- a/app/modules/sport_competition/utils/data_exporter/sport_quotas_exporter.py +++ b/app/modules/sport_competition/utils/data_exporter/sport_quotas_exporter.py @@ -23,7 +23,7 @@ def build_data_rows( school_sports_quotas: list[schemas_sport_competition.SchoolSportQuota], schools: list[schemas_sport_competition.SchoolExtension], -) -> tuple[list, list[int]]: +) -> tuple[list, list[int]]: # ty:ignore[missing-type-argument] data_rows = [] school_dict = {school.school_id: school.school for school in schools} @@ -50,7 +50,7 @@ def build_data_rows( def write_fixed_headers( worksheet: xlsxwriter.Workbook.worksheet_class, - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): for col_idx, column in enumerate(FIXED_COLUMNS): worksheet.write(0, col_idx, column, formats["header"]["base"]) @@ -58,9 +58,9 @@ def write_fixed_headers( def write_to_excel( workbook: xlsxwriter.Workbook, - data_rows: list, + data_rows: list, # ty:ignore[missing-type-argument] thick_columns: list[int], - formats: dict, + formats: dict, # ty:ignore[missing-type-argument] ): worksheet = workbook.add_worksheet("Quotas") write_fixed_headers(worksheet, formats) diff --git a/app/types/scheduler.py b/app/types/scheduler.py index b5ae28635e..e32662fba6 100644 --- a/app/types/scheduler.py +++ b/app/types/scheduler.py @@ -143,7 +143,7 @@ def __init__(self): # ArqWorker, in charge of scheduling and executing tasks self.worker: Worker | None = None # Task will contain the asyncio task that runs the worker - self.task: asyncio.Task | None = None + self.task: asyncio.Task[None] | None = None async def start( self, @@ -254,7 +254,7 @@ def __init__(self): # ArqWorker, in charge of scheduling and executing tasks self.worker: Worker | None = None # Task will contain the asyncio task that runs the worker - self.task: asyncio.Task | None = None + self.task: asyncio.Task[None] | None = None # Pointer to the get_db dependency async def start( diff --git a/app/types/sqlalchemy.py b/app/types/sqlalchemy.py index 003e15c746..029a02b0e7 100644 --- a/app/types/sqlalchemy.py +++ b/app/types/sqlalchemy.py @@ -15,7 +15,7 @@ SessionLocalType = Callable[[], AsyncSession] -class TZDateTime(TypeDecorator): +class TZDateTime(TypeDecorator[datetime.datetime]): """ Custom SQLAlchemy type for storing timezone-aware timestamps as timezone-naive UTC timestamps. We use this custom type because sqlite doesn't support datetime with timezone diff --git a/app/types/websocket.py b/app/types/websocket.py index 81ec37a23f..0d68c99662 100644 --- a/app/types/websocket.py +++ b/app/types/websocket.py @@ -76,7 +76,7 @@ def __init__(self, settings: Settings): # We keep a reference to the listening tasks for each room # to be able to stop listening to a room when there is no more connection - self.listening_tasks: dict[HyperionWebsocketsRoom, asyncio.Task] = {} + self.listening_tasks: dict[HyperionWebsocketsRoom, asyncio.Task[None]] = {} async def connect_broadcaster(self): await self.broadcaster.connect() diff --git a/app/utils/initialization.py b/app/utils/initialization.py index 9db227985b..b9a95f060b 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -300,7 +300,7 @@ def drop_db_sync(conn: Connection): async def use_lock_for_workers[**P, R]( job_function: Callable[P, R], key: str, - redis_client: redis.Redis | None, + redis_client: redis.Redis[bytes] | None, number_of_workers: int, logger: logging.Logger, unlock_key: str | None = None, diff --git a/app/utils/loggers_tools/matrix_handler.py b/app/utils/loggers_tools/matrix_handler.py index d95a2b6ae1..45084f42db 100644 --- a/app/utils/loggers_tools/matrix_handler.py +++ b/app/utils/loggers_tools/matrix_handler.py @@ -1,13 +1,13 @@ import logging -from logging import StreamHandler -from typing import override +from logging import LogRecord, StreamHandler +from typing import TextIO, override from app.utils.communication.matrix import Matrix hyperion_error_logger = logging.getLogger("hyperion.error") -class MatrixHandler(StreamHandler): +class MatrixHandler(StreamHandler[TextIO]): """ A logging handler which sends log records to a Matrix server. @@ -38,7 +38,7 @@ def __init__( ) @override - def emit(self, record): + def emit(self, record: LogRecord) -> None: if self.enabled: msg = self.format(record) try: diff --git a/app/utils/loggers_tools/s3_handler.py b/app/utils/loggers_tools/s3_handler.py index a64b0f0825..3a706fa766 100644 --- a/app/utils/loggers_tools/s3_handler.py +++ b/app/utils/loggers_tools/s3_handler.py @@ -1,7 +1,7 @@ import string from datetime import UTC, datetime -from logging import StreamHandler -from typing import override +from logging import LogRecord, StreamHandler +from typing import TextIO, override from app.types.s3_access import S3Access from app.utils.tools import get_random_string @@ -9,7 +9,7 @@ alphanum = string.ascii_lowercase + string.digits -class S3LogHandler(StreamHandler): +class S3LogHandler(StreamHandler[TextIO]): def __init__( self, failure_logger: str, @@ -28,7 +28,7 @@ def __init__( ) @override - def emit(self, record): + def emit(self, record: LogRecord) -> None: filename: str | None = getattr(record, "s3_filename", None) subfolder: str | None = getattr(record, "s3_subfolder", None) retention: int = getattr(record, "s3_retention", 0) diff --git a/app/utils/redis.py b/app/utils/redis.py index 6765c034e5..1614cccc9e 100644 --- a/app/utils/redis.py +++ b/app/utils/redis.py @@ -1,7 +1,7 @@ import redis -def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): +def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int): """Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id""" # Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm. nb = redis_client.incr(key) @@ -17,12 +17,12 @@ def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): return True, False -def locker_get(redis_client: redis.Redis, key: str): +def locker_get(redis_client: redis.Redis[bytes], key: str): value = redis_client.get(key) if value is None: return False return bool(int(value)) -def locker_set(redis_client: redis.Redis, key: str, lock: bool): +def locker_set(redis_client: redis.Redis[bytes], key: str, lock: bool): redis_client.set(key, int(lock)) diff --git a/app/utils/state.py b/app/utils/state.py index 0d5d755749..7d61a94235 100644 --- a/app/utils/state.py +++ b/app/utils/state.py @@ -30,7 +30,7 @@ class GlobalState(TypedDict): # Database session creator SessionLocal: SessionLocalType # We may not have a Redis Client if it was not configured - redis_client: redis.Redis | None + redis_client: redis.Redis[bytes] | None scheduler: Scheduler ws_manager: WebsocketConnectionManager notification_manager: NotificationManager @@ -79,12 +79,12 @@ def init_SessionLocal(engine: AsyncEngine) -> SessionLocalType: def init_redis_client( settings: Settings, hyperion_error_logger: logging.Logger, -) -> redis.Redis | None: +) -> redis.Redis[bytes] | None: """ Initialize the Redis client if the settings specify a Redis connection. Returns None if Redis is not configured. """ - redis_client: redis.Redis | None = None + redis_client: redis.Redis[bytes] | None = None if settings.REDIS_HOST is not None and settings.REDIS_HOST != "": try: redis_client = redis.Redis( @@ -101,7 +101,7 @@ def init_redis_client( return redis_client -def disconnect_redis_client(redis_client: redis.Redis | None) -> None: +def disconnect_redis_client(redis_client: redis.Redis[bytes] | None) -> None: if redis_client is not None: redis_client.flushdb() redis_client.close() diff --git a/migrations/versions/21-fix_phonebook.py b/migrations/versions/21-fix_phonebook.py index afc365be6d..390fb6d96c 100644 --- a/migrations/versions/21-fix_phonebook.py +++ b/migrations/versions/21-fix_phonebook.py @@ -40,7 +40,7 @@ class RoleTags(Enum): resp_part = "Respo Partenariats" -def define_order_of_memberships(memberships: list[sa.Row[Any]]) -> list[list]: +def define_order_of_memberships(memberships: list[sa.Row[Any]]) -> list[list[Any]]: """ Use tag systeme to introduce a coherent member_order to older membership """ From 3e18e71e054126411b1f6d46333f8ffcefd473c9 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:41:54 +0200 Subject: [PATCH 07/15] Lint --- migrations/README.md | 2 +- migrations/env.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/README.md b/migrations/README.md index 384779380c..934de205bd 100644 --- a/migrations/README.md +++ b/migrations/README.md @@ -75,7 +75,7 @@ from sqlalchemy.dialects import postgresql You need to use: ```python -postgresql.ENUM(name="availableassociationmembership", create_type=False), +postgresql.ENUM(name="availableassociationmembership", create_type=False) ``` ### Convert str id to uuid diff --git a/migrations/env.py b/migrations/env.py index 1c95504451..df4d60be76 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -124,7 +124,7 @@ def run_migrations_online() -> None: We requires a *connection* (`Connection` or `AsyncConnection`) object. You may obtain one from an *connectable* calling the [`connect` method](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Engine.connect). """ - connection: None | Connection | AsyncConnection = config.attributes.get( + connection: Connection | AsyncConnection | None = config.attributes.get( "connection", None, ) From 3ac4e02b97fa144446a6ca2a8ee84b3ddf94b402 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:42:02 +0200 Subject: [PATCH 08/15] Upgrade dependencies --- requirements-dev.txt | 12 ++++++------ requirements.txt | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index fbd1abf7f1..2e601d7d5b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,17 +1,17 @@ -r requirements.txt aiosqlite==0.22.1 -boto3-stubs[essential]==1.43.24 +boto3-stubs[essential]==1.43.56 google-auth-stubs==0.3.0 mypy[faster-cache]==2.1.0 pytest-alembic==0.12.1 pytest-asyncio==1.4.0 pytest-cov==7.1.0 pytest-mock==3.15.1 -pytest==9.0.3 -ruff==0.15.16 -types-Authlib==1.6.11.20260518 -types-fpdf2==2.8.4.20260518 +pytest==9.1.1 +ruff==0.16.0 ty==0.0.63 +types-Authlib==1.7.2.20260724 +types-fpdf2==2.8.4.20260724 types-psutil==7.2.2.20260518 types-redis==4.6.0.20241004 -types-requests==2.33.0.20260518 \ No newline at end of file +types-requests==2.33.0.20260712 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f487459191..300532cc12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,38 +1,38 @@ -alembic==1.18.4 # database migrations -anyio==4.13.0 +alembic==1.18.5 # database migrations +anyio==4.14.2 arq==0.28.0 # Scheduler asyncpg==0.31.0 # PostgreSQL adapter for asynchronous operations authlib==1.7.2 bcrypt==5.0.0 # password hashing -boto3==1.43.24 # S3 storage +boto3==1.43.56 # S3 storage broadcaster==0.3.1 # Working with websockets with multiple workers. calypsso-proximapp==2.8.0 email-validator==2.3.0 -Faker==40.21.0 -fastapi[standard]==0.136.3 -firebase-admin==7.4.0 # Firebase is used for push notification -google-api-python-client==2.197.0 +Faker==40.36.0 +fastapi[standard]==0.140.0 +firebase-admin==7.5.0 # Firebase is used for push notification +google-api-python-client==2.198.0 google-auth-oauthlib==1.4.0 helloasso-python==1.0.8 -httpx2==2.3.0 -icalendar==7.1.2 +httpx2==2.9.1 +icalendar==7.2.2 jellyfish==1.2.1 # String Matching Jinja2==3.1.6 # template engine for html files -phonenumbers==9.0.32 # Used for phone number validation +phonenumbers==9.0.35 # Used for phone number validation psutil==7.2.2 # psutil is used to determine the number of Hyperion workers psycopg[binary]==3.3.4 # PostgreSQL adapter for *synchronous* operations at startup (database initializations & migrations) pydantic-extra-types==2.11.1 -pydantic-settings==2.14.1 +pydantic-settings==2.14.2 pydantic==2.13.4 pyjwt[crypto]==2.13.0 # generate and verify the JWT tokens, imported as `jwt` -PyMuPDF==1.27.2.3 # PDF processing, imported as `fitz` -pypdf==6.13.0 +PyMuPDF==1.28.0 # PDF processing, imported as `fitz` +pypdf==6.14.2 python-multipart==0.0.32 # a form data parser, as oauth flow requires form-data parameters redis==5.3.1 requests==2.34.2 sqlalchemy-utils == 0.42.1 # used to identify foreign keys when merging users -SQLAlchemy[asyncio]==2.0.50 # [asyncio] allows greenlet to be installed on Apple M1 devices. +SQLAlchemy[asyncio]==2.0.51 # [asyncio] allows greenlet to be installed on Apple M1 devices. unidecode==1.4.0 -uvicorn[standard]==0.49.0 +uvicorn[standard]==0.51.0 weasyprint==69.0 # HTML to PDF converter xlsxwriter==3.2.9 From a5d0613ea08445c6d01bcb79f2fe8c0cda959d5f Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:54:48 +0200 Subject: [PATCH 09/15] Remove types-redis Should not be used with redis >= 5, see https://pypi.org/project/types-redis/ --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2e601d7d5b..20ad638198 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,5 +13,4 @@ ty==0.0.63 types-Authlib==1.7.2.20260724 types-fpdf2==2.8.4.20260724 types-psutil==7.2.2.20260518 -types-redis==4.6.0.20241004 types-requests==2.33.0.20260712 \ No newline at end of file From a743a56ba7de17bfd68411cd3c004846040dcf0f Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:58:52 +0200 Subject: [PATCH 10/15] Redis should be awaited --- app/app.py | 6 +++--- app/dependencies.py | 2 +- app/modules/amap/endpoints_amap.py | 24 ++++++++++++------------ app/modules/raffle/endpoints_raffle.py | 16 ++++++++-------- app/utils/initialization.py | 2 +- app/utils/redis.py | 14 +++++++------- app/utils/state.py | 8 ++++---- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/app.py b/app/app.py index 306b7c1ca0..6ad83dd332 100644 --- a/app/app.py +++ b/app/app.py @@ -523,7 +523,7 @@ async def init_lifespan( hyperion_error_logger=hyperion_error_logger, ) - redis_client: Redis[bytes] | None = app.dependency_overrides.get( + redis_client: Redis | None = app.dependency_overrides.get( get_redis_client, get_redis_client, )() @@ -697,12 +697,12 @@ async def logging_middleware( port = request.client.port client_address = f"{ip_address}:{port}" - redis_client: Redis[bytes] | None = get_redis_client_dependency() + redis_client: Redis | None = get_redis_client_dependency() # We test the ip address with the redis limiter process = True if redis_client and settings.ENABLE_RATE_LIMITER: # If redis is configured - process, log = limiter( + process, log = await limiter( redis_client, ip_address, settings.REDIS_LIMIT, diff --git a/app/dependencies.py b/app/dependencies.py index 297b287e3e..04533ef8de 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -239,7 +239,7 @@ async def get_unsafe_db() -> AsyncGenerator[AsyncSession]: yield db -def get_redis_client() -> redis.Redis[bytes] | None: +def get_redis_client() -> redis.Redis | None: """ Dependency that returns the redis client diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index fb10ae127a..432afe8039 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -458,7 +458,7 @@ async def get_order_by_id( async def add_order_to_delievery( order: schemas_amap.OrderBase, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -532,12 +532,12 @@ async def add_order_to_delievery( raise HTTPException(status_code=400, detail="You can't order nothing") redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.add_order_to_delivery( @@ -600,7 +600,7 @@ async def add_order_to_delievery( delivery_name=orderret.delivery.name, ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.patch( @@ -611,7 +611,7 @@ async def edit_order_from_delivery( order_id: str, order: schemas_amap.OrderEdit, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -694,12 +694,12 @@ async def edit_order_from_delivery( raise HTTPException(status_code=404, detail="No cash found") redis_key = "amap_" + previous_order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.edit_order_with_products( @@ -727,7 +727,7 @@ async def edit_order_from_delivery( ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.delete( @@ -737,7 +737,7 @@ async def edit_order_from_delivery( async def remove_order( order_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -778,12 +778,12 @@ async def remove_order( redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.remove_order( @@ -801,7 +801,7 @@ async def remove_order( return Response(status_code=204) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 4fe4b76ca5..9c9047f15e 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -480,7 +480,7 @@ async def get_tickets( async def buy_ticket( pack_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.access_raffle]), ), @@ -522,13 +522,13 @@ async def buy_ticket( redis_key = "raffle_" + user.id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: new_amount = balance.balance - pack_ticket.price @@ -550,7 +550,7 @@ async def buy_ticket( return tickets finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.get( @@ -981,7 +981,7 @@ async def edit_cash_by_id( user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.manage_cash]), ), - redis_client: Redis[bytes] = Depends(get_redis_client), + redis_client: Redis = Depends(get_redis_client), ): """ Edit cash for an user. This will add the balance to the current balance. @@ -1002,12 +1002,12 @@ async def edit_cash_by_id( redis_key = "raffle_" + user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=403, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_raffle.edit_cash( @@ -1016,7 +1016,7 @@ async def edit_cash_by_id( db=db, ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/utils/initialization.py b/app/utils/initialization.py index b9a95f060b..9db227985b 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -300,7 +300,7 @@ def drop_db_sync(conn: Connection): async def use_lock_for_workers[**P, R]( job_function: Callable[P, R], key: str, - redis_client: redis.Redis[bytes] | None, + redis_client: redis.Redis | None, number_of_workers: int, logger: logging.Logger, unlock_key: str | None = None, diff --git a/app/utils/redis.py b/app/utils/redis.py index 1614cccc9e..ffddf1fb6d 100644 --- a/app/utils/redis.py +++ b/app/utils/redis.py @@ -1,12 +1,12 @@ import redis -def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int): +async def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): """Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id""" # Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm. - nb = redis_client.incr(key) + nb = await redis_client.incr(key) if nb == 1: - redis_client.expire(key, window) + await redis_client.expire(key, window) elif nb == limit: return ( False, @@ -17,12 +17,12 @@ def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int) return True, False -def locker_get(redis_client: redis.Redis[bytes], key: str): - value = redis_client.get(key) +async def locker_get(redis_client: redis.Redis, key: str): + value = await redis_client.get(key) if value is None: return False return bool(int(value)) -def locker_set(redis_client: redis.Redis[bytes], key: str, lock: bool): - redis_client.set(key, int(lock)) +async def locker_set(redis_client: redis.Redis, key: str, lock: bool): + await redis_client.set(key, int(lock)) diff --git a/app/utils/state.py b/app/utils/state.py index 7d61a94235..0d5d755749 100644 --- a/app/utils/state.py +++ b/app/utils/state.py @@ -30,7 +30,7 @@ class GlobalState(TypedDict): # Database session creator SessionLocal: SessionLocalType # We may not have a Redis Client if it was not configured - redis_client: redis.Redis[bytes] | None + redis_client: redis.Redis | None scheduler: Scheduler ws_manager: WebsocketConnectionManager notification_manager: NotificationManager @@ -79,12 +79,12 @@ def init_SessionLocal(engine: AsyncEngine) -> SessionLocalType: def init_redis_client( settings: Settings, hyperion_error_logger: logging.Logger, -) -> redis.Redis[bytes] | None: +) -> redis.Redis | None: """ Initialize the Redis client if the settings specify a Redis connection. Returns None if Redis is not configured. """ - redis_client: redis.Redis[bytes] | None = None + redis_client: redis.Redis | None = None if settings.REDIS_HOST is not None and settings.REDIS_HOST != "": try: redis_client = redis.Redis( @@ -101,7 +101,7 @@ def init_redis_client( return redis_client -def disconnect_redis_client(redis_client: redis.Redis[bytes] | None) -> None: +def disconnect_redis_client(redis_client: redis.Redis | None) -> None: if redis_client is not None: redis_client.flushdb() redis_client.close() From 8170171c37a4d10d311777211282dc4ba2b449d3 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:59:18 +0200 Subject: [PATCH 11/15] Indicates arq requires redis<6 --- requirements.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 300532cc12..4e2db19980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ alembic==1.18.5 # database migrations anyio==4.14.2 -arq==0.28.0 # Scheduler +arq==0.28.0 # Scheduler, [not maintained, see https://github.com/python-arq/arq/issues/510] asyncpg==0.31.0 # PostgreSQL adapter for asynchronous operations authlib==1.7.2 bcrypt==5.0.0 # password hashing @@ -24,11 +24,11 @@ psycopg[binary]==3.3.4 # PostgreSQL adapter for *synchronous* opera pydantic-extra-types==2.11.1 pydantic-settings==2.14.2 pydantic==2.13.4 -pyjwt[crypto]==2.13.0 # generate and verify the JWT tokens, imported as `jwt` -PyMuPDF==1.28.0 # PDF processing, imported as `fitz` +pyjwt[crypto]==2.13.0 # generate and verify the JWT tokens, imported as `jwt` +PyMuPDF==1.28.0 # PDF processing, imported as `fitz` pypdf==6.14.2 -python-multipart==0.0.32 # a form data parser, as oauth flow requires form-data parameters -redis==5.3.1 +python-multipart==0.0.32 # a form data parser, as oauth flow requires form-data parameters +redis==5.3.1 # arq requires redis<6 requests==2.34.2 sqlalchemy-utils == 0.42.1 # used to identify foreign keys when merging users SQLAlchemy[asyncio]==2.0.51 # [asyncio] allows greenlet to be installed on Apple M1 devices. From fc66325532416b51845b58b879c860b80773a7b5 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:00:22 +0200 Subject: [PATCH 12/15] Revert "Redis should be awaited" This reverts commit a743a56ba7de17bfd68411cd3c004846040dcf0f. --- app/app.py | 6 +++--- app/dependencies.py | 2 +- app/modules/amap/endpoints_amap.py | 24 ++++++++++++------------ app/modules/raffle/endpoints_raffle.py | 16 ++++++++-------- app/utils/initialization.py | 2 +- app/utils/redis.py | 14 +++++++------- app/utils/state.py | 8 ++++---- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/app.py b/app/app.py index 6ad83dd332..306b7c1ca0 100644 --- a/app/app.py +++ b/app/app.py @@ -523,7 +523,7 @@ async def init_lifespan( hyperion_error_logger=hyperion_error_logger, ) - redis_client: Redis | None = app.dependency_overrides.get( + redis_client: Redis[bytes] | None = app.dependency_overrides.get( get_redis_client, get_redis_client, )() @@ -697,12 +697,12 @@ async def logging_middleware( port = request.client.port client_address = f"{ip_address}:{port}" - redis_client: Redis | None = get_redis_client_dependency() + redis_client: Redis[bytes] | None = get_redis_client_dependency() # We test the ip address with the redis limiter process = True if redis_client and settings.ENABLE_RATE_LIMITER: # If redis is configured - process, log = await limiter( + process, log = limiter( redis_client, ip_address, settings.REDIS_LIMIT, diff --git a/app/dependencies.py b/app/dependencies.py index 04533ef8de..297b287e3e 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -239,7 +239,7 @@ async def get_unsafe_db() -> AsyncGenerator[AsyncSession]: yield db -def get_redis_client() -> redis.Redis | None: +def get_redis_client() -> redis.Redis[bytes] | None: """ Dependency that returns the redis client diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index 432afe8039..fb10ae127a 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -458,7 +458,7 @@ async def get_order_by_id( async def add_order_to_delievery( order: schemas_amap.OrderBase, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -532,12 +532,12 @@ async def add_order_to_delievery( raise HTTPException(status_code=400, detail="You can't order nothing") redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or await locker_get( + if not isinstance(redis_client, Redis) or locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - await locker_set(redis_client=redis_client, key=redis_key, lock=True) + locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.add_order_to_delivery( @@ -600,7 +600,7 @@ async def add_order_to_delievery( delivery_name=orderret.delivery.name, ) finally: - await locker_set(redis_client=redis_client, key=redis_key, lock=False) + locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.patch( @@ -611,7 +611,7 @@ async def edit_order_from_delivery( order_id: str, order: schemas_amap.OrderEdit, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -694,12 +694,12 @@ async def edit_order_from_delivery( raise HTTPException(status_code=404, detail="No cash found") redis_key = "amap_" + previous_order.user_id - if not isinstance(redis_client, Redis) or await locker_get( + if not isinstance(redis_client, Redis) or locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - await locker_set(redis_client=redis_client, key=redis_key, lock=True) + locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.edit_order_with_products( @@ -727,7 +727,7 @@ async def edit_order_from_delivery( ) finally: - await locker_set(redis_client=redis_client, key=redis_key, lock=False) + locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.delete( @@ -737,7 +737,7 @@ async def edit_order_from_delivery( async def remove_order( order_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -778,12 +778,12 @@ async def remove_order( redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or await locker_get( + if not isinstance(redis_client, Redis) or locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - await locker_set(redis_client=redis_client, key=redis_key, lock=True) + locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.remove_order( @@ -801,7 +801,7 @@ async def remove_order( return Response(status_code=204) finally: - await locker_set(redis_client=redis_client, key=redis_key, lock=False) + locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 9c9047f15e..4fe4b76ca5 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -480,7 +480,7 @@ async def get_tickets( async def buy_ticket( pack_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis | None = Depends(get_redis_client), + redis_client: Redis[bytes] | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.access_raffle]), ), @@ -522,13 +522,13 @@ async def buy_ticket( redis_key = "raffle_" + user.id - if not isinstance(redis_client, Redis) or await locker_get( + if not isinstance(redis_client, Redis) or locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - await locker_set(redis_client=redis_client, key=redis_key, lock=True) + locker_set(redis_client=redis_client, key=redis_key, lock=True) try: new_amount = balance.balance - pack_ticket.price @@ -550,7 +550,7 @@ async def buy_ticket( return tickets finally: - await locker_set(redis_client=redis_client, key=redis_key, lock=False) + locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.get( @@ -981,7 +981,7 @@ async def edit_cash_by_id( user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.manage_cash]), ), - redis_client: Redis = Depends(get_redis_client), + redis_client: Redis[bytes] = Depends(get_redis_client), ): """ Edit cash for an user. This will add the balance to the current balance. @@ -1002,12 +1002,12 @@ async def edit_cash_by_id( redis_key = "raffle_" + user_id - if not isinstance(redis_client, Redis) or await locker_get( + if not isinstance(redis_client, Redis) or locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=403, detail="Too fast !") - await locker_set(redis_client=redis_client, key=redis_key, lock=True) + locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_raffle.edit_cash( @@ -1016,7 +1016,7 @@ async def edit_cash_by_id( db=db, ) finally: - await locker_set(redis_client=redis_client, key=redis_key, lock=False) + locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/utils/initialization.py b/app/utils/initialization.py index 9db227985b..b9a95f060b 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -300,7 +300,7 @@ def drop_db_sync(conn: Connection): async def use_lock_for_workers[**P, R]( job_function: Callable[P, R], key: str, - redis_client: redis.Redis | None, + redis_client: redis.Redis[bytes] | None, number_of_workers: int, logger: logging.Logger, unlock_key: str | None = None, diff --git a/app/utils/redis.py b/app/utils/redis.py index ffddf1fb6d..1614cccc9e 100644 --- a/app/utils/redis.py +++ b/app/utils/redis.py @@ -1,12 +1,12 @@ import redis -async def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): +def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int): """Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id""" # Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm. - nb = await redis_client.incr(key) + nb = redis_client.incr(key) if nb == 1: - await redis_client.expire(key, window) + redis_client.expire(key, window) elif nb == limit: return ( False, @@ -17,12 +17,12 @@ async def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): return True, False -async def locker_get(redis_client: redis.Redis, key: str): - value = await redis_client.get(key) +def locker_get(redis_client: redis.Redis[bytes], key: str): + value = redis_client.get(key) if value is None: return False return bool(int(value)) -async def locker_set(redis_client: redis.Redis, key: str, lock: bool): - await redis_client.set(key, int(lock)) +def locker_set(redis_client: redis.Redis[bytes], key: str, lock: bool): + redis_client.set(key, int(lock)) diff --git a/app/utils/state.py b/app/utils/state.py index 0d5d755749..7d61a94235 100644 --- a/app/utils/state.py +++ b/app/utils/state.py @@ -30,7 +30,7 @@ class GlobalState(TypedDict): # Database session creator SessionLocal: SessionLocalType # We may not have a Redis Client if it was not configured - redis_client: redis.Redis | None + redis_client: redis.Redis[bytes] | None scheduler: Scheduler ws_manager: WebsocketConnectionManager notification_manager: NotificationManager @@ -79,12 +79,12 @@ def init_SessionLocal(engine: AsyncEngine) -> SessionLocalType: def init_redis_client( settings: Settings, hyperion_error_logger: logging.Logger, -) -> redis.Redis | None: +) -> redis.Redis[bytes] | None: """ Initialize the Redis client if the settings specify a Redis connection. Returns None if Redis is not configured. """ - redis_client: redis.Redis | None = None + redis_client: redis.Redis[bytes] | None = None if settings.REDIS_HOST is not None and settings.REDIS_HOST != "": try: redis_client = redis.Redis( @@ -101,7 +101,7 @@ def init_redis_client( return redis_client -def disconnect_redis_client(redis_client: redis.Redis | None) -> None: +def disconnect_redis_client(redis_client: redis.Redis[bytes] | None) -> None: if redis_client is not None: redis_client.flushdb() redis_client.close() From f484f9d3cd1a93571eda0208b64ea2ef09ac04e2 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:01:00 +0200 Subject: [PATCH 13/15] Redis is not Generic --- app/app.py | 4 ++-- app/dependencies.py | 2 +- app/utils/redis.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/app.py b/app/app.py index 306b7c1ca0..a8f083aecb 100644 --- a/app/app.py +++ b/app/app.py @@ -523,7 +523,7 @@ async def init_lifespan( hyperion_error_logger=hyperion_error_logger, ) - redis_client: Redis[bytes] | None = app.dependency_overrides.get( + redis_client: Redis | None = app.dependency_overrides.get( get_redis_client, get_redis_client, )() @@ -697,7 +697,7 @@ async def logging_middleware( port = request.client.port client_address = f"{ip_address}:{port}" - redis_client: Redis[bytes] | None = get_redis_client_dependency() + redis_client: Redis | None = get_redis_client_dependency() # We test the ip address with the redis limiter process = True diff --git a/app/dependencies.py b/app/dependencies.py index 297b287e3e..04533ef8de 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -239,7 +239,7 @@ async def get_unsafe_db() -> AsyncGenerator[AsyncSession]: yield db -def get_redis_client() -> redis.Redis[bytes] | None: +def get_redis_client() -> redis.Redis | None: """ Dependency that returns the redis client diff --git a/app/utils/redis.py b/app/utils/redis.py index 1614cccc9e..6765c034e5 100644 --- a/app/utils/redis.py +++ b/app/utils/redis.py @@ -1,7 +1,7 @@ import redis -def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int): +def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): """Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id""" # Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm. nb = redis_client.incr(key) @@ -17,12 +17,12 @@ def limiter(redis_client: redis.Redis[bytes], key: str, limit: int, window: int) return True, False -def locker_get(redis_client: redis.Redis[bytes], key: str): +def locker_get(redis_client: redis.Redis, key: str): value = redis_client.get(key) if value is None: return False return bool(int(value)) -def locker_set(redis_client: redis.Redis[bytes], key: str, lock: bool): +def locker_set(redis_client: redis.Redis, key: str, lock: bool): redis_client.set(key, int(lock)) From 63547db72f4eb3d9ab5cbc924899428ff2e5a7fc Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:36:12 +0200 Subject: [PATCH 14/15] Async Redis --- app/app.py | 4 ++-- app/dependencies.py | 6 +++--- app/modules/amap/endpoints_amap.py | 26 +++++++++++++------------- app/modules/raffle/endpoints_raffle.py | 18 +++++++++--------- app/utils/initialization.py | 14 +++++++------- app/utils/redis.py | 16 ++++++++-------- app/utils/state.py | 22 ++++++++++++---------- tests/commons.py | 2 +- 8 files changed, 55 insertions(+), 53 deletions(-) diff --git a/app/app.py b/app/app.py index a8f083aecb..3a3ffdb64c 100644 --- a/app/app.py +++ b/app/app.py @@ -52,7 +52,7 @@ from app.utils.state import LifespanState if TYPE_CHECKING: - from redis import Redis + from redis.asyncio import Redis from app.types.factory import Factory @@ -702,7 +702,7 @@ async def logging_middleware( # We test the ip address with the redis limiter process = True if redis_client and settings.ENABLE_RATE_LIMITER: # If redis is configured - process, log = limiter( + process, log = await limiter( redis_client, ip_address, settings.REDIS_LIMIT, diff --git a/app/dependencies.py b/app/dependencies.py index 04533ef8de..aa520e770f 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -14,7 +14,7 @@ async def get_users(db: AsyncSession = Depends(get_db)): from uuid import UUID import calypsso -import redis +import redis.asyncio as redis import starlette import starlette.datastructures from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request @@ -101,7 +101,7 @@ async def init_state( SessionLocal = init_SessionLocal(engine) - redis_client = init_redis_client( + redis_client = await init_redis_client( settings=settings, hyperion_error_logger=hyperion_error_logger, ) @@ -145,7 +145,7 @@ async def disconnect_state( This methode should be called as a dependency as tests may need to run additional steps """ - disconnect_redis_client(GLOBAL_STATE["redis_client"]) + await disconnect_redis_client(GLOBAL_STATE["redis_client"]) await disconnect_scheduler(GLOBAL_STATE["scheduler"]) await disconnect_websocket_connection_manager(GLOBAL_STATE["ws_manager"]) diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index fb10ae127a..d1e4a37180 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -3,7 +3,7 @@ from datetime import UTC, datetime from fastapi import Depends, HTTPException, Response -from redis import Redis +from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession from app.core.groups.groups_type import AccountType @@ -458,7 +458,7 @@ async def get_order_by_id( async def add_order_to_delievery( order: schemas_amap.OrderBase, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -532,12 +532,12 @@ async def add_order_to_delievery( raise HTTPException(status_code=400, detail="You can't order nothing") redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.add_order_to_delivery( @@ -600,7 +600,7 @@ async def add_order_to_delievery( delivery_name=orderret.delivery.name, ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.patch( @@ -611,7 +611,7 @@ async def edit_order_from_delivery( order_id: str, order: schemas_amap.OrderEdit, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -694,12 +694,12 @@ async def edit_order_from_delivery( raise HTTPException(status_code=404, detail="No cash found") redis_key = "amap_" + previous_order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.edit_order_with_products( @@ -727,7 +727,7 @@ async def edit_order_from_delivery( ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.delete( @@ -737,7 +737,7 @@ async def edit_order_from_delivery( async def remove_order( order_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([AmapPermissions.access_amap]), ), @@ -778,12 +778,12 @@ async def remove_order( redis_key = "amap_" + order.user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_amap.remove_order( @@ -801,7 +801,7 @@ async def remove_order( return Response(status_code=204) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 4fe4b76ca5..d902e88256 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -3,7 +3,7 @@ from fastapi import Depends, HTTPException from fastapi.responses import FileResponse -from redis import Redis +from redis.asyncio import Redis from sqlalchemy.ext.asyncio import AsyncSession from app.core.groups import cruds_groups @@ -480,7 +480,7 @@ async def get_tickets( async def buy_ticket( pack_id: str, db: AsyncSession = Depends(get_db), - redis_client: Redis[bytes] | None = Depends(get_redis_client), + redis_client: Redis | None = Depends(get_redis_client), user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.access_raffle]), ), @@ -522,13 +522,13 @@ async def buy_ticket( redis_key = "raffle_" + user.id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=429, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: new_amount = balance.balance - pack_ticket.price @@ -550,7 +550,7 @@ async def buy_ticket( return tickets finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.get( @@ -981,7 +981,7 @@ async def edit_cash_by_id( user: models_users.CoreUser = Depends( is_user_allowed_to([RafflePermissions.manage_cash]), ), - redis_client: Redis[bytes] = Depends(get_redis_client), + redis_client: Redis = Depends(get_redis_client), ): """ Edit cash for an user. This will add the balance to the current balance. @@ -1002,12 +1002,12 @@ async def edit_cash_by_id( redis_key = "raffle_" + user_id - if not isinstance(redis_client, Redis) or locker_get( + if not isinstance(redis_client, Redis) or await locker_get( redis_client=redis_client, key=redis_key, ): raise HTTPException(status_code=403, detail="Too fast !") - locker_set(redis_client=redis_client, key=redis_key, lock=True) + await locker_set(redis_client=redis_client, key=redis_key, lock=True) try: await cruds_raffle.edit_cash( @@ -1016,7 +1016,7 @@ async def edit_cash_by_id( db=db, ) finally: - locker_set(redis_client=redis_client, key=redis_key, lock=False) + await locker_set(redis_client=redis_client, key=redis_key, lock=False) @module.router.post( diff --git a/app/utils/initialization.py b/app/utils/initialization.py index b9a95f060b..92e7c2c8cb 100644 --- a/app/utils/initialization.py +++ b/app/utils/initialization.py @@ -6,7 +6,7 @@ from typing import ParamSpec, TypeVar import psutil -import redis +import redis.asyncio as redis from pydantic import ValidationError from sqlalchemy import Connection, MetaData, delete, select from sqlalchemy.engine import Engine, create_engine @@ -300,7 +300,7 @@ def drop_db_sync(conn: Connection): async def use_lock_for_workers[**P, R]( job_function: Callable[P, R], key: str, - redis_client: redis.Redis[bytes] | None, + redis_client: redis.Redis | None, number_of_workers: int, logger: logging.Logger, unlock_key: str | None = None, @@ -340,7 +340,7 @@ async def use_lock_for_workers[**P, R]( ): await execute_async_or_sync_method(job_function, *args, **kwargs) - elif redis_client.set(key, "1", nx=True, ex=120): + elif await redis_client.set(key, "1", nx=True, ex=120): # We acquired the lock, we execute the function logger.info(f"Running {getattr(job_function, '__name__', repr(job_function))}") @@ -348,19 +348,19 @@ async def use_lock_for_workers[**P, R]( if unlock_key is not None: # We set the unlock_key for other workers to resume operation - redis_client.set(unlock_key, "1") + await redis_client.set(unlock_key, "1") # After 60 seconds we remove the key for both performance and reloading issues # we assume other jobs won't take more than 60 seconds and will check this key before expiration - redis_client.expire(unlock_key, 60) + await redis_client.expire(unlock_key, 60) # After 60 seconds we remove the key for both performance and reloading issues # we assume other jobs won't take more than 60 seconds and will check this key before expiration - redis_client.expire(key, 60) + await redis_client.expire(key, 60) 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: + while await redis_client.get(unlock_key) is None: logger.debug( f"Waiting for {getattr(job_function, '__name__', repr(job_function))} to finish", ) diff --git a/app/utils/redis.py b/app/utils/redis.py index 6765c034e5..ee273e5c06 100644 --- a/app/utils/redis.py +++ b/app/utils/redis.py @@ -1,12 +1,12 @@ -import redis +import redis.asyncio as redis -def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): +async def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): """Simple fixed window rate limiter, returns a couple of booleans: the first is True if the request can be processed, False otherwise; the second indicates if an alert should be issued. key should be an ip address or a user id""" # Fixed window: see https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm. - nb = redis_client.incr(key) + nb = await redis_client.incr(key) if nb == 1: - redis_client.expire(key, window) + await redis_client.expire(key, window) elif nb == limit: return ( False, @@ -17,12 +17,12 @@ def limiter(redis_client: redis.Redis, key: str, limit: int, window: int): return True, False -def locker_get(redis_client: redis.Redis, key: str): - value = redis_client.get(key) +async def locker_get(redis_client: redis.Redis, key: str): + value = await redis_client.get(key) if value is None: return False return bool(int(value)) -def locker_set(redis_client: redis.Redis, key: str, lock: bool): - redis_client.set(key, int(lock)) +async def locker_set(redis_client: redis.Redis, key: str, lock: bool): + await redis_client.set(key, int(lock)) diff --git a/app/utils/state.py b/app/utils/state.py index 7d61a94235..792cdcbc89 100644 --- a/app/utils/state.py +++ b/app/utils/state.py @@ -3,7 +3,8 @@ from typing import Any, TypedDict import calypsso -import redis +import redis.asyncio as redis +from redis.exceptions import ConnectionError as RedisConnectionError from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, @@ -30,7 +31,7 @@ class GlobalState(TypedDict): # Database session creator SessionLocal: SessionLocalType # We may not have a Redis Client if it was not configured - redis_client: redis.Redis[bytes] | None + redis_client: redis.Redis | None scheduler: Scheduler ws_manager: WebsocketConnectionManager notification_manager: NotificationManager @@ -76,15 +77,15 @@ def init_SessionLocal(engine: AsyncEngine) -> SessionLocalType: ) -def init_redis_client( +async def init_redis_client( settings: Settings, hyperion_error_logger: logging.Logger, -) -> redis.Redis[bytes] | None: +) -> redis.Redis | None: """ Initialize the Redis client if the settings specify a Redis connection. Returns None if Redis is not configured. """ - redis_client: redis.Redis[bytes] | None = None + redis_client: redis.Redis | None = None if settings.REDIS_HOST is not None and settings.REDIS_HOST != "": try: redis_client = redis.Redis( @@ -93,18 +94,19 @@ def init_redis_client( password=settings.REDIS_PASSWORD, socket_keepalive=True, ) - redis_client.ping() # Test the connection - except redis.exceptions.ConnectionError: + await redis_client.ping() # Test the connection + except RedisConnectionError: hyperion_error_logger.exception( "Redis connection error: Check the Redis configuration or the Redis server", ) + return redis_client -def disconnect_redis_client(redis_client: redis.Redis[bytes] | None) -> None: +async def disconnect_redis_client(redis_client: redis.Redis | None) -> None: if redis_client is not None: - redis_client.flushdb() - redis_client.close() + await redis_client.flushdb() + await redis_client.close() async def init_scheduler( diff --git a/tests/commons.py b/tests/commons.py index 5bf017e64b..7622d1087e 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -57,7 +57,7 @@ async def override_init_state( SessionLocal = init_test_SessionLocal(engine=engine) - redis_client = init_redis_client( + redis_client = await init_redis_client( settings=settings, hyperion_error_logger=hyperion_error_logger, ) From e29af13699203a793eafeb16f3c15229b53f46cd Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:36:31 +0200 Subject: [PATCH 15/15] CorePermissionsFactory: Add missing depends_on --- app/core/permissions/factory_permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/core/permissions/factory_permissions.py b/app/core/permissions/factory_permissions.py index 6617c6df08..691fdf6184 100644 --- a/app/core/permissions/factory_permissions.py +++ b/app/core/permissions/factory_permissions.py @@ -8,6 +8,8 @@ class CorePermissionsFactory(Factory): + depends_on = [] + @classmethod async def run(cls, db: AsyncSession, settings: Settings) -> None: for permission in permissions_list: