diff --git a/backend/domain/entities/exceptions/already_existing_project_exception.py b/backend/domain/entities/exceptions/already_existing_project_exception.py new file mode 100644 index 0000000..942ec1e --- /dev/null +++ b/backend/domain/entities/exceptions/already_existing_project_exception.py @@ -0,0 +1,11 @@ +# Philippe Stepniewski +from fastapi import HTTPException + + +class AlreadyExistingProjectException(HTTPException): + """Exception raised when a project with the same name already exists""" + + def __init__(self, project_name: str): + self.message = f"A project with the name '{project_name}' already exists" + + super().__init__(status_code=409, detail=self.message) diff --git a/backend/domain/use_cases/projects_usecases.py b/backend/domain/use_cases/projects_usecases.py index 4b1fad5..8d624ac 100644 --- a/backend/domain/use_cases/projects_usecases.py +++ b/backend/domain/use_cases/projects_usecases.py @@ -1,12 +1,14 @@ # Philippe Stepniewski from loguru import logger +from backend.domain.entities.exceptions.already_existing_project_exception import AlreadyExistingProjectException from backend.domain.entities.project import Project from backend.domain.ports.object_storage_handler import ObjectStorageHandler from backend.domain.ports.project_db_handler import ProjectDbHandler from backend.domain.use_cases.deploy_registry import deploy_registry from backend.domain.use_cases.deployed_models import _remove_project_namespace from backend.infrastructure.log_events_handler_json_adapter import LogEventsHandlerFileAdapter +from backend.infrastructure.project_sqlite_db_handler import ProjectDoesntExistError EVENT_LOGGER = LogEventsHandlerFileAdapter() @@ -25,6 +27,11 @@ def list_projects_for_user(user: str, project_db_handler: ProjectDbHandler) -> l def add_project(project_db_handler: ProjectDbHandler, project: Project, object_storage: ObjectStorageHandler) -> bool: + try: + project_db_handler.get_project(project.name) + raise AlreadyExistingProjectException(project_name=project.name) + except ProjectDoesntExistError: + pass deploy_registry(project.name) if project.batch_enabled: object_storage.ensure_project_space(project.name) diff --git a/backend/domain/use_cases/templates/risk_level_suggestion.txt b/backend/domain/use_cases/templates/risk_level_suggestion.txt new file mode 100644 index 0000000..8411199 --- /dev/null +++ b/backend/domain/use_cases/templates/risk_level_suggestion.txt @@ -0,0 +1,33 @@ +Tu es un expert en réglementation de l'Intelligence Artificielle, spécialisé dans le Règlement européen sur l'IA (AI Act — Règlement (UE) 2024/1689). + +Voici la fiche AI Act d'un modèle de machine learning déployé sur une plateforme MLOps : +--- +{ai_act_card_markdown} +--- + +À partir de toutes les informations disponibles (nom du modèle, projet, description, métriques, paramètres, signature, cas d'usage), détermine le niveau de risque AI Act le plus approprié parmi les quatre catégories suivantes : + +1. **unacceptable** — Système interdit (Article 5). Exemples : scoring social, manipulation subliminale, exploitation de vulnérabilités, notation sociale par les autorités publiques, identification biométrique à distance en temps réel dans l'espace public (sauf exceptions). + +2. **high** — Risque élevé (Article 6 + Annexe III). Exemples : + - Identification biométrique et catégorisation des personnes physiques + - Gestion et exploitation d'infrastructures critiques (énergie, transport, eau, gaz) + - Éducation et formation professionnelle (accès, évaluation, orientation) + - Emploi, gestion de la main-d'œuvre et accès au travail indépendant (recrutement, promotion, licenciement) + - Accès aux services essentiels et prestations (crédit, assurance, services publics, aide sociale) + - Répression pénale (évaluation de risque de récidive, profilage, détection de mensonges) + - Migration, asile et gestion des frontières + - Administration de la justice et processus démocratiques + +3. **limited** — Risque limité (Article 50). Systèmes nécessitant des obligations de transparence : chatbots, deepfakes, systèmes de génération de contenu, systèmes d'émotion ou de catégorisation biométrique. + +4. **minimal** — Risque minimal. Aucune obligation spécifique. Exemples : filtres anti-spam, recommandations de contenu non critique, optimisation logistique interne, maintenance prédictive industrielle. + +Réponds UNIQUEMENT avec le format JSON suivant, sans aucun texte avant ou après : + +```json +{{ + "suggested_risk_level": "", + "justification": "" +}} +``` diff --git a/backend/infrastructure/project_pgsql_db_handler.py b/backend/infrastructure/project_pgsql_db_handler.py index 1a2fe45..4a15f8b 100644 --- a/backend/infrastructure/project_pgsql_db_handler.py +++ b/backend/infrastructure/project_pgsql_db_handler.py @@ -1,11 +1,8 @@ -import logging - import psycopg2 from backend.domain.entities.project import Project from backend.domain.ports.project_db_handler import ProjectDbHandler from backend.infrastructure.project_sqlite_db_handler import ( - ProjectAlreadyExistError, ProjectDoesntExistError, map_rows_to_projects, ) @@ -60,11 +57,6 @@ def get_project(self, name) -> Project | None: def add_project(self, project: Project) -> bool: connection = self._connect() - try: - self.get_project(name=project.name) - raise ProjectAlreadyExistError(name=project.name, message="Project with same name already exists") - except ProjectDoesntExistError: - logging.info("Project name not used yet, ok") try: cursor = connection.cursor() query = """ diff --git a/backend/infrastructure/project_sqlite_db_handler.py b/backend/infrastructure/project_sqlite_db_handler.py index 91557aa..74fbc7e 100644 --- a/backend/infrastructure/project_sqlite_db_handler.py +++ b/backend/infrastructure/project_sqlite_db_handler.py @@ -1,4 +1,3 @@ -import logging import sqlite3 from backend.domain.entities.project import Project @@ -73,11 +72,6 @@ def get_project(self, name) -> Project | None: def add_project(self, project: Project) -> bool: connection = sqlite3.connect(self.db_path) - try: - self.get_project(name=project.name) - raise ProjectAlreadyExistError(name=project.name, message="Project with same name already exists") - except ProjectDoesntExistError: - logging.info("Project name not used yet, ok") try: cursor = connection.cursor() cursor.execute( diff --git a/tests/test_unitaires/use_cases/test_projects_usecases.py b/tests/test_unitaires/use_cases/test_projects_usecases.py index 16c1e93..1971643 100644 --- a/tests/test_unitaires/use_cases/test_projects_usecases.py +++ b/tests/test_unitaires/use_cases/test_projects_usecases.py @@ -3,12 +3,14 @@ from unittest.mock import MagicMock, patch import pytest +from fastapi import HTTPException # Set required env var before importing the module that needs it os.environ.setdefault("PATH_LOG_EVENTS", "/tmp/test_log_events") from backend.domain.entities.project import Project from backend.domain.use_cases.projects_usecases import add_project, remove_project, update_project_batch_enabled +from backend.infrastructure.project_sqlite_db_handler import ProjectDoesntExistError @pytest.fixture @@ -17,6 +19,7 @@ def mock_project_db_handler(): handler.add_project.return_value = True handler.remove_project.return_value = True handler.update_batch_enabled.return_value = True + handler.get_project.side_effect = ProjectDoesntExistError(message="Project doesn't exist") return handler @@ -51,6 +54,49 @@ def test_add_project_without_batch_does_not_create_storage_space( mock_project_db_handler.add_project.assert_called_once_with(project) +@patch("backend.domain.use_cases.projects_usecases.deploy_registry") +def test_add_project_raises_409_when_project_already_exists( + mock_deploy_registry, mock_project_db_handler, mock_object_storage +): + existing_project = Project( + name="test-project", owner="owner", scope="scope", data_perimeter="perimeter", batch_enabled=False + ) + mock_project_db_handler.get_project.side_effect = None + mock_project_db_handler.get_project.return_value = existing_project + + project = Project( + name="test-project", owner="other-owner", scope="scope", data_perimeter="perimeter", batch_enabled=False + ) + + with pytest.raises(HTTPException) as exc_info: + add_project(mock_project_db_handler, project, mock_object_storage) + + assert exc_info.value.status_code == 409 + assert "test-project" in exc_info.value.detail + + +@patch("backend.domain.use_cases.projects_usecases.deploy_registry") +def test_add_project_does_not_deploy_registry_when_project_already_exists( + mock_deploy_registry, mock_project_db_handler, mock_object_storage +): + existing_project = Project( + name="test-project", owner="owner", scope="scope", data_perimeter="perimeter", batch_enabled=True + ) + mock_project_db_handler.get_project.side_effect = None + mock_project_db_handler.get_project.return_value = existing_project + + project = Project( + name="test-project", owner="other-owner", scope="scope", data_perimeter="perimeter", batch_enabled=True + ) + + with pytest.raises(HTTPException): + add_project(mock_project_db_handler, project, mock_object_storage) + + mock_deploy_registry.assert_not_called() + mock_object_storage.ensure_project_space.assert_not_called() + mock_project_db_handler.add_project.assert_not_called() + + @patch("backend.domain.use_cases.projects_usecases._remove_project_namespace") def test_remove_project_cleans_up_storage(mock_remove_ns, mock_project_db_handler, mock_object_storage): remove_project(mock_project_db_handler, "test-project", mock_object_storage)