From 08dafdb49086cb411a06b8a210ba95fdf7fc8bd9 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:14:05 +0200 Subject: [PATCH 1/5] Allows to enable only specific modules # Conflicts: # app/module.py --- app/api.py | 11 ---- app/app.py | 20 +++--- app/core/checkout/endpoints_checkout.py | 4 +- app/core/core_endpoints/endpoints_core.py | 11 +++- app/core/utils/config.py | 4 ++ app/module.py | 79 +++++++++++++++-------- config.template.yaml | 4 ++ tests/test_factories.py | 4 +- 8 files changed, 85 insertions(+), 52 deletions(-) delete mode 100644 app/api.py diff --git a/app/api.py b/app/api.py deleted file mode 100644 index 7a326fe35c..0000000000 --- a/app/api.py +++ /dev/null @@ -1,11 +0,0 @@ -"""File defining all the routes for the module, to configure the router""" - -from fastapi import APIRouter - -from app.module import all_modules - -api_router = APIRouter() - - -for module in all_modules: - api_router.include_router(module.router) diff --git a/app/app.py b/app/app.py index 671ce44258..2b254295ac 100644 --- a/app/app.py +++ b/app/app.py @@ -12,7 +12,7 @@ import alembic.migration as alembic_migration import redis from calypsso import get_calypsso_app -from fastapi import FastAPI, HTTPException, Request, Response, status +from fastapi import APIRouter, FastAPI, HTTPException, Request, Response, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware @@ -24,7 +24,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from app import api from app.core.core_endpoints import coredata_core, models_core from app.core.google_api.google_api import GoogleAPI from app.core.groups import models_groups @@ -41,7 +40,7 @@ get_redis_client, init_state, ) -from app.module import all_modules, module_list +from app.module import get_all_modules, get_module_list, init_module_list from app.types.exceptions import ( ContentHTTPException, GoogleAPIInvalidCredentialsError, @@ -240,7 +239,7 @@ async def run_factories( hyperion_error_logger.info("Startup: Factories enabled") # Importing the core_factory at the beginning of the factories. factories_list: list[Factory] = [] - for module in all_modules: + for module in get_all_modules(): if module.factory: factories_list.append(module.factory) hyperion_error_logger.info( @@ -302,7 +301,7 @@ def initialize_module_visibility( new_modules = [ module - for module in module_list + for module in get_module_list() if module.root not in module_awareness.roots ] # Is run to create default module visibilities or when the table is empty @@ -345,7 +344,7 @@ def initialize_module_visibility( ) initialization.set_core_data_sync( coredata_core.ModuleVisibilityAwareness( - roots=[module.root for module in module_list], + roots=[module.root for module in get_module_list()], ), db, ) @@ -365,7 +364,7 @@ async def initialize_notification_topics( ) -> None: existing_topics = await get_notification_topic(db=db) existing_topics_id = [topic.id for topic in existing_topics] - for module in all_modules: + for module in get_all_modules(): if module.registred_topics: for registred_topic in module.registred_topics: if registred_topic.id not in existing_topics_id: @@ -623,13 +622,18 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[LifespanState, None]: hyperion_error_logger=hyperion_error_logger, ) + init_module_list(settings=settings) + # Initialize app app = FastAPI( title="Hyperion", version=settings.HYPERION_VERSION, lifespan=lifespan, ) - app.include_router(api.api_router) + api_router = APIRouter() + for module in get_all_modules(): + api_router.include_router(module.router) + app.include_router(api_router) use_route_path_as_operation_ids(app) app.add_middleware( diff --git a/app/core/checkout/endpoints_checkout.py b/app/core/checkout/endpoints_checkout.py index a223f4a18c..1db5dd66f7 100644 --- a/app/core/checkout/endpoints_checkout.py +++ b/app/core/checkout/endpoints_checkout.py @@ -14,7 +14,7 @@ NotificationResultContent, ) from app.dependencies import get_db -from app.module import all_modules +from app.module import get_all_modules from app.types.module import CoreModule router = APIRouter(tags=["Checkout"]) @@ -134,7 +134,7 @@ async def webhook( # If a callback is defined for the module, we want to call it try: - for module in all_modules: + for module in get_all_modules(): if module.root == checkout.module: if module.payment_callback is None: hyperion_error_logger.info( diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index 09301a23eb..2a6a182614 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -14,7 +14,7 @@ is_user, is_user_super_admin, ) -from app.module import module_list +from app.module import get_module_list from app.types.module import CoreModule from app.utils.tools import is_group_id_valid, patch_identity_in_text @@ -219,7 +219,7 @@ async def get_module_visibility( """ return_module_visibilities = [] - for module in module_list: + for module in get_module_list(): allowed_group_ids = await cruds_core.get_allowed_groups_by_root( root=module.root, db=db, @@ -247,6 +247,7 @@ async def get_module_visibility( async def get_user_modules_visibility( db: AsyncSession = Depends(get_db), user: models_users.CoreUser = Depends(is_user()), + settings: Settings = Depends(get_settings), ): """ Get group user accessible root @@ -254,7 +255,11 @@ async def get_user_modules_visibility( **This endpoint is only usable by everyone** """ - return await cruds_core.get_modules_by_user(user=user, db=db) + modules = await cruds_core.get_modules_by_user(user=user, db=db) + if settings.RESTRICT_TO_MODULES: + return [module for module in modules if module in settings.RESTRICT_TO_MODULES] + + return modules @router.post( diff --git a/app/core/utils/config.py b/app/core/utils/config.py index a0244975fc..57ab51672e 100644 --- a/app/core/utils/config.py +++ b/app/core/utils/config.py @@ -220,6 +220,10 @@ def settings_customise_sources( # If self registration is disabled, users will need to be invited by an administrator to be able to register ALLOW_SELF_REGISTRATION: bool = True + # Restrict to a list of module roots + # CoreModules can not be disabled + RESTRICT_TO_MODULES: list[str] | None = None + ############################ # PostgreSQL configuration # ############################ diff --git a/app/module.py b/app/module.py index 8cce42d9e1..22cfa4565c 100644 --- a/app/module.py +++ b/app/module.py @@ -2,37 +2,64 @@ import logging from pathlib import Path +from app.core.utils.config import Settings from app.types.module import CoreModule, Module hyperion_error_logger = logging.getLogger("hyperion.error") -module_list: list[Module] = [] -core_module_list: list[CoreModule] = [] -all_modules: list[CoreModule] = [] - -for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): - endpoint_module = importlib.import_module( - ".".join(endpoints_file.with_suffix("").parts), - ) - if hasattr(endpoint_module, "module"): - module: Module = endpoint_module.module - module_list.append(module) - all_modules.append(module) - else: - hyperion_error_logger.error( - f"Module {endpoints_file} does not declare a module. It won't be enabled.", +_module_list: list[Module] = [] +_core_module_list: list[CoreModule] = [] +_all_modules: list[Module | CoreModule] = [] + + +def init_module_list(settings: Settings): + module_list = [] + for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): + endpoint_module = importlib.import_module( + ".".join(endpoints_file.with_suffix("").parts), ) + if hasattr(endpoint_module, "module"): + module: Module = endpoint_module.module + module_list.append(module) + else: + hyperion_error_logger.error( + f"Module {endpoints_file} does not declare a module. It won't be enabled.", + ) + if settings.RESTRICT_TO_MODULES: + existing_module_roots = [module.root for module in module_list] + for root in settings.RESTRICT_TO_MODULES: + if root not in existing_module_roots: + raise ValueError() + for module in module_list: + if ( + settings.RESTRICT_TO_MODULES + and module.root not in settings.RESTRICT_TO_MODULES + ): + continue + _module_list.append(module) + _all_modules.append(module) -for endpoints_file in Path().glob("app/core/*/endpoints_*.py"): - endpoint_module = importlib.import_module( - ".".join(endpoints_file.with_suffix("").parts), - ) - if hasattr(endpoint_module, "core_module"): - core_module: CoreModule = endpoint_module.core_module - core_module_list.append(core_module) - all_modules.append(core_module) - else: - hyperion_error_logger.error( - f"Core module {endpoints_file} does not declare a core module. It won't be enabled.", + for endpoints_file in Path().glob("app/core/*/endpoints_*.py"): + endpoint_module = importlib.import_module( + ".".join(endpoints_file.with_suffix("").parts), ) + if hasattr(endpoint_module, "core_module"): + core_module: CoreModule = endpoint_module.core_module + _core_module_list.append(core_module) + else: + hyperion_error_logger.error( + f"Core module {endpoints_file} does not declare a core module. It won't be enabled.", + ) + + +def get_module_list() -> list[Module]: + return _module_list + + +def get_core_module_list() -> list[CoreModule]: + return _core_module_list + + +def get_all_modules() -> list[Module | CoreModule]: + return get_module_list() + get_core_module_list() diff --git a/config.template.yaml b/config.template.yaml index 11c1ab9fa9..79d2fc07fb 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -54,6 +54,10 @@ SQLITE_DB: "app.db" # If True, will print all SQL queries in the console DATABASE_DEBUG: False +# Restrict to a list of module roots +# CoreModules can not be disabled +#RESTRICT_TO_MODULES: [] + ##################################### # SMTP configuration using starttls # ##################################### diff --git a/tests/test_factories.py b/tests/test_factories.py index 75b96fe810..dfd583913c 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from app.module import all_modules +from app.module import get_all_modules from tests.commons import get_TestingSessionLocal @@ -9,7 +9,7 @@ async def test_factories(client: TestClient) -> None: async with get_TestingSessionLocal()() as db: factories = [ - module.factory for module in all_modules if module.factory is not None + module.factory for module in get_all_modules() if module.factory is not None ] for factory in factories: assert not await factory.should_run( From af264867d4256f9a2bcbf48faa2aa551638e0571 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:37:14 +0200 Subject: [PATCH 2/5] fixup --- app/app.py | 6 +++--- app/module.py | 6 +++++- app/types/exceptions.py | 5 +++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/app.py b/app/app.py index 2b254295ac..e33a782120 100644 --- a/app/app.py +++ b/app/app.py @@ -298,6 +298,7 @@ def initialize_module_visibility( coredata_core.ModuleVisibilityAwareness, db, ) + known_roots = module_awareness.roots new_modules = [ module @@ -310,6 +311,7 @@ def initialize_module_visibility( f"Startup: Some modules visibility settings are empty, initializing them ({[module.root for module in new_modules]})", ) for module in new_modules: + known_roots.append(module.root) if module.default_allowed_groups_ids is not None: for group_id in module.default_allowed_groups_ids: module_group_visibility = models_core.ModuleGroupVisibility( @@ -343,9 +345,7 @@ def initialize_module_visibility( f"Startup: Could not add module visibility {module.root} in the database: {error}", ) initialization.set_core_data_sync( - coredata_core.ModuleVisibilityAwareness( - roots=[module.root for module in get_module_list()], - ), + coredata_core.ModuleVisibilityAwareness(roots=known_roots), db, ) hyperion_error_logger.info( diff --git a/app/module.py b/app/module.py index 22cfa4565c..ba992f6114 100644 --- a/app/module.py +++ b/app/module.py @@ -3,6 +3,7 @@ from pathlib import Path from app.core.utils.config import Settings +from app.types.exceptions import InvalidModuleRootInDotenvError from app.types.module import CoreModule, Module hyperion_error_logger = logging.getLogger("hyperion.error") @@ -13,6 +14,9 @@ def init_module_list(settings: Settings): + _module_list.clear() + _core_module_list.clear() + module_list = [] for endpoints_file in Path().glob("app/modules/*/endpoints_*.py"): endpoint_module = importlib.import_module( @@ -30,7 +34,7 @@ def init_module_list(settings: Settings): existing_module_roots = [module.root for module in module_list] for root in settings.RESTRICT_TO_MODULES: if root not in existing_module_roots: - raise ValueError() + raise InvalidModuleRootInDotenvError(root) for module in module_list: if ( settings.RESTRICT_TO_MODULES diff --git a/app/types/exceptions.py b/app/types/exceptions.py index 0d5dee2ea8..52f8d623b6 100644 --- a/app/types/exceptions.py +++ b/app/types/exceptions.py @@ -32,6 +32,11 @@ def __init__(self): super().__init__("Google API is not configured in dotenv") +class InvalidModuleRootInDotenvError(Exception): + def __init__(self, root: str): + super().__init__(f"Module root {root} does not exist") + + class ContentHTTPException(HTTPException): """ A custom HTTPException allowing to return custom content. From f7193ab43352cb40770313397b65b5ad5f32cc7c Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Thu, 28 Aug 2025 01:16:30 +0200 Subject: [PATCH 3/5] Fix mock --- tests/test_checkout.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 716bd0a6e4..8b1c9b437a 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -308,8 +308,8 @@ async def test_webhook_payment_callback( factory=None, ) mocker.patch( - "app.core.checkout.endpoints_checkout.all_modules", - [test_module], + "app.module.get_all_modules", + return_value=[test_module], ) response = client.post( @@ -350,8 +350,8 @@ async def test_webhook_payment_callback_fail( factory=None, ) mocker.patch( - "app.core.checkout.endpoints_checkout.all_modules", - [test_module], + "app.module.get_all_modules", + return_value=[test_module], ) mocked_hyperion_security_logger = mocker.patch( From 8483440364bd0849d34eedad367bcb6c181047c7 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Thu, 28 Aug 2025 01:24:06 +0200 Subject: [PATCH 4/5] test --- tests/test_checkout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 8b1c9b437a..68e45e034e 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -308,7 +308,7 @@ async def test_webhook_payment_callback( factory=None, ) mocker.patch( - "app.module.get_all_modules", + "app.core.checkout.endpoints_checkout.get_all_modules", return_value=[test_module], ) @@ -350,7 +350,7 @@ async def test_webhook_payment_callback_fail( factory=None, ) mocker.patch( - "app.module.get_all_modules", + "app.core.checkout.endpoints_checkout.get_all_modules", return_value=[test_module], ) From 570cbe3b46ebc96a86dab0e92283eb8360a83604 Mon Sep 17 00:00:00 2001 From: armanddidierjean <95971503+armanddidierjean@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:35:04 +0100 Subject: [PATCH 5/5] Use _all_modules list --- app/module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/module.py b/app/module.py index ba992f6114..5e506b8bc7 100644 --- a/app/module.py +++ b/app/module.py @@ -51,6 +51,7 @@ def init_module_list(settings: Settings): if hasattr(endpoint_module, "core_module"): core_module: CoreModule = endpoint_module.core_module _core_module_list.append(core_module) + _all_modules.append(core_module) else: hyperion_error_logger.error( f"Core module {endpoints_file} does not declare a core module. It won't be enabled.", @@ -66,4 +67,4 @@ def get_core_module_list() -> list[CoreModule]: def get_all_modules() -> list[Module | CoreModule]: - return get_module_list() + get_core_module_list() + return _all_modules