Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions backend/domain/use_cases/projects_usecases.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions backend/domain/use_cases/templates/risk_level_suggestion.txt
Original file line number Diff line number Diff line change
@@ -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": "<unacceptable|high|limited|minimal>",
"justification": "<Justification détaillée en 3 à 5 phrases, expliquant pourquoi ce niveau de risque est approprié en référençant les articles et annexes pertinents du règlement.>"
}}
```
8 changes: 0 additions & 8 deletions backend/infrastructure/project_pgsql_db_handler.py
Original file line number Diff line number Diff line change
@@ -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,
)
Expand Down Expand Up @@ -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 = """
Expand Down
6 changes: 0 additions & 6 deletions backend/infrastructure/project_sqlite_db_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import logging
import sqlite3

from backend.domain.entities.project import Project
Expand Down Expand Up @@ -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(
Expand Down
46 changes: 46 additions & 0 deletions tests/test_unitaires/use_cases/test_projects_usecases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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)
Expand Down
Loading