From fb9030443baccfbfca0e133d65c8a68304fd98a2 Mon Sep 17 00:00:00 2001 From: florianlega <104771295+florianlega@users.noreply.github.com> Date: Tue, 24 Dec 2024 14:15:32 +0100 Subject: [PATCH] feat: Add new method to update project workflow (#1836) --- docs/sdk/project_workflow.md | 3 ++ mkdocs.yml | 1 + .../kili_api_gateway/kili_api_gateway.py | 4 ++ .../project_workflow/__init__.py | 0 .../project_workflow/mappers.py | 12 ++++++ .../project_workflow/operations.py | 12 ++++++ .../project_workflow/operations_mixin.py | 37 +++++++++++++++++++ .../project_workflow/types.py | 11 ++++++ src/kili/client.py | 2 + .../entrypoints/mutations/asset/__init__.py | 1 - .../presentation/client/project_workflow.py | 37 +++++++++++++++++++ .../use_cases/project_workflow/__init__.py | 25 +++++++++++++ .../presentation/test_project_workflow.py | 29 +++++++++++++++ .../use_cases/test_project_workflow.py | 31 ++++++++++++++++ 14 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 docs/sdk/project_workflow.md create mode 100644 src/kili/adapters/kili_api_gateway/project_workflow/__init__.py create mode 100644 src/kili/adapters/kili_api_gateway/project_workflow/mappers.py create mode 100644 src/kili/adapters/kili_api_gateway/project_workflow/operations.py create mode 100644 src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py create mode 100644 src/kili/adapters/kili_api_gateway/project_workflow/types.py create mode 100644 src/kili/presentation/client/project_workflow.py create mode 100644 src/kili/use_cases/project_workflow/__init__.py create mode 100644 tests/integration/presentation/test_project_workflow.py create mode 100644 tests/integration/use_cases/test_project_workflow.py diff --git a/docs/sdk/project_workflow.md b/docs/sdk/project_workflow.md new file mode 100644 index 000000000..e64ce866e --- /dev/null +++ b/docs/sdk/project_workflow.md @@ -0,0 +1,3 @@ +# Project Workflow module + +::: kili.presentation.client.project_workflow.ProjectWorkflowClientMethods diff --git a/mkdocs.yml b/mkdocs.yml index 8003171e1..89fb2d83b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - Project: sdk/project.md - Project User: sdk/project_user.md - Project Version: sdk/project_version.md + - Project Workflow: sdk/project_workflow.md - Tag: sdk/tag.md - User: sdk/user.md - Command Line Interface: diff --git a/src/kili/adapters/kili_api_gateway/kili_api_gateway.py b/src/kili/adapters/kili_api_gateway/kili_api_gateway.py index ef0b37383..52ba61b68 100644 --- a/src/kili/adapters/kili_api_gateway/kili_api_gateway.py +++ b/src/kili/adapters/kili_api_gateway/kili_api_gateway.py @@ -17,6 +17,9 @@ OrganizationOperationMixin, ) from kili.adapters.kili_api_gateway.project.operations_mixin import ProjectOperationMixin +from kili.adapters.kili_api_gateway.project_workflow.operations_mixin import ( + ProjectWorkflowOperationMixin, +) from kili.adapters.kili_api_gateway.tag import TagOperationMixin from kili.adapters.kili_api_gateway.user.operation_mixin import UserOperationMixin from kili.core.graphql.graphql_client import GraphQLClient @@ -32,6 +35,7 @@ class KiliAPIGateway( NotificationOperationMixin, OrganizationOperationMixin, ProjectOperationMixin, + ProjectWorkflowOperationMixin, TagOperationMixin, UserOperationMixin, EventOperationMixin, diff --git a/src/kili/adapters/kili_api_gateway/project_workflow/__init__.py b/src/kili/adapters/kili_api_gateway/project_workflow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/kili/adapters/kili_api_gateway/project_workflow/mappers.py b/src/kili/adapters/kili_api_gateway/project_workflow/mappers.py new file mode 100644 index 000000000..91cd6be42 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/project_workflow/mappers.py @@ -0,0 +1,12 @@ +"""GraphQL payload data mappers for project operations.""" + +from typing import Dict + +from .types import ProjectWorkflowDataKiliAPIGatewayInput + + +def project_input_mapper(data: ProjectWorkflowDataKiliAPIGatewayInput) -> Dict: + """Build the GraphQL ProjectWorfklowData variable to be sent in an operation.""" + return { + "enforceStepSeparation": data.enforce_step_separation, + } diff --git a/src/kili/adapters/kili_api_gateway/project_workflow/operations.py b/src/kili/adapters/kili_api_gateway/project_workflow/operations.py new file mode 100644 index 000000000..babf8cfa9 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/project_workflow/operations.py @@ -0,0 +1,12 @@ +"""GraphQL Project Workflow operations.""" + + +def get_update_project_workflow_mutation(fragment: str) -> str: + """Return the GraphQL editProjectWorkflowSettings mutation.""" + return f""" + mutation editProjectWorkflowSettings($input: EditProjectWorkflowSettingsInput!) {{ + data: editProjectWorkflowSettings(input: $input) {{ + {fragment} + }} + }} + """ diff --git a/src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py b/src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py new file mode 100644 index 000000000..8c23e8503 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py @@ -0,0 +1,37 @@ +"""Mixin extending Kili API Gateway class with Projects related operations.""" + +from typing import Dict + +from kili.adapters.kili_api_gateway.base import BaseOperationMixin +from kili.adapters.kili_api_gateway.helpers.queries import ( + fragment_builder, +) +from kili.domain.project import ProjectId + +from .mappers import project_input_mapper +from .operations import ( + get_update_project_workflow_mutation, +) +from .types import ProjectWorkflowDataKiliAPIGatewayInput + + +class ProjectWorkflowOperationMixin(BaseOperationMixin): + """Mixin extending Kili API Gateway class with Projects workflow related operations.""" + + def update_project_workflow( + self, + project_id: ProjectId, + project_workflow_data: ProjectWorkflowDataKiliAPIGatewayInput, + ) -> Dict: + """Update properties in a project workflow.""" + project_workflow_input = project_input_mapper(data=project_workflow_data) + + fields = tuple(name for name, val in project_workflow_input.items() if val is not None) + fragment = fragment_builder(fields) + mutation = get_update_project_workflow_mutation(fragment) + + project_workflow_input["projectId"] = project_id + + variables = {"input": project_workflow_input} + result = self.graphql_client.execute(mutation, variables) + return result["data"] diff --git a/src/kili/adapters/kili_api_gateway/project_workflow/types.py b/src/kili/adapters/kili_api_gateway/project_workflow/types.py new file mode 100644 index 000000000..30f231863 --- /dev/null +++ b/src/kili/adapters/kili_api_gateway/project_workflow/types.py @@ -0,0 +1,11 @@ +"""Types for the ProjectWorkflow-related Kili API gateway functions.""" + +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class ProjectWorkflowDataKiliAPIGatewayInput: + """ProjectWorkflow input data for Kili API Gateway.""" + + enforce_step_separation: Optional[bool] diff --git a/src/kili/client.py b/src/kili/client.py index 500725c32..fc67dc339 100644 --- a/src/kili/client.py +++ b/src/kili/client.py @@ -32,6 +32,7 @@ from kili.presentation.client.notification import NotificationClientMethods from kili.presentation.client.organization import OrganizationClientMethods from kili.presentation.client.project import ProjectClientMethods +from kili.presentation.client.project_workflow import ProjectWorkflowClientMethods from kili.presentation.client.tag import TagClientMethods from kili.presentation.client.user import UserClientMethods from kili.use_cases.api_key import ApiKeyUseCases @@ -68,6 +69,7 @@ class Kili( # pylint: disable=too-many-ancestors,too-many-instance-attributes NotificationClientMethods, OrganizationClientMethods, ProjectClientMethods, + ProjectWorkflowClientMethods, TagClientMethods, UserClientMethods, ): diff --git a/src/kili/entrypoints/mutations/asset/__init__.py b/src/kili/entrypoints/mutations/asset/__init__.py index 648f83201..c4dab5910 100644 --- a/src/kili/entrypoints/mutations/asset/__init__.py +++ b/src/kili/entrypoints/mutations/asset/__init__.py @@ -212,7 +212,6 @@ def assign_assets_to_labelers( external_ids: Optional[List[str]] = None, project_id: Optional[str] = None, ) -> List[Dict[str, Any]]: - # pylint: disable=line-too-long """Assign a list of assets to a list of labelers. Args: diff --git a/src/kili/presentation/client/project_workflow.py b/src/kili/presentation/client/project_workflow.py new file mode 100644 index 000000000..da4f7b43e --- /dev/null +++ b/src/kili/presentation/client/project_workflow.py @@ -0,0 +1,37 @@ +"""Client presentation methods for project workflow.""" + +from typing import Any, Dict, Optional + +from typeguard import typechecked + +from kili.domain.project import ProjectId +from kili.use_cases.project_workflow import ProjectWorkflowUseCases + +from .base import BaseClientMethods + + +class ProjectWorkflowClientMethods(BaseClientMethods): + """Client presentation methods for project workflow.""" + + @typechecked + def update_project_workflow( + self, + project_id: str, + enforce_step_separation: Optional[bool] = None, + ) -> Dict[str, Any]: + """Update properties of a project workflow. + + Args: + project_id: Id of the project. + enforce_step_separation: Prevents the same user from being assigned to + multiple steps in the workflow for a same asset, + ensuring independent review and labeling processes + + Returns: + A dict with the changed properties which indicates if the mutation was successful, + else an error message. + """ + return ProjectWorkflowUseCases(self.kili_api_gateway).update_project_workflow( + project_id=ProjectId(project_id), + enforce_step_separation=enforce_step_separation, + ) diff --git a/src/kili/use_cases/project_workflow/__init__.py b/src/kili/use_cases/project_workflow/__init__.py new file mode 100644 index 000000000..a628bda0a --- /dev/null +++ b/src/kili/use_cases/project_workflow/__init__.py @@ -0,0 +1,25 @@ +"""Project use cases.""" + +from typing import Dict, Optional + +from kili.adapters.kili_api_gateway.project_workflow.types import ( + ProjectWorkflowDataKiliAPIGatewayInput, +) +from kili.domain.project import ProjectId +from kili.use_cases.base import BaseUseCases + + +class ProjectWorkflowUseCases(BaseUseCases): + """ProjectWorkflow use cases.""" + + def update_project_workflow( + self, + project_id: ProjectId, + enforce_step_separation: Optional[bool] = None, + ) -> Dict[str, object]: + """Update properties in a project workflow.""" + project_workflow_data = ProjectWorkflowDataKiliAPIGatewayInput( + enforce_step_separation=enforce_step_separation, + ) + + return self._kili_api_gateway.update_project_workflow(project_id, project_workflow_data) diff --git a/tests/integration/presentation/test_project_workflow.py b/tests/integration/presentation/test_project_workflow.py new file mode 100644 index 000000000..e3c181f71 --- /dev/null +++ b/tests/integration/presentation/test_project_workflow.py @@ -0,0 +1,29 @@ +import pytest_mock + +from kili.adapters.kili_api_gateway.kili_api_gateway import KiliAPIGateway +from kili.adapters.kili_api_gateway.project_workflow.operations import ( + get_update_project_workflow_mutation, +) +from kili.presentation.client.project_workflow import ProjectWorkflowClientMethods + + +def test_when_updating_project_workflow_then_it_returns_updated_project_workflow( + mocker: pytest_mock.MockerFixture, +): + kili = ProjectWorkflowClientMethods() + kili.kili_api_gateway = KiliAPIGateway( + graphql_client=mocker.MagicMock(), http_client=mocker.MagicMock() + ) + # Given + project_id = "fake_proj_id" + + # When + kili.update_project_workflow(project_id, enforce_step_separation=False) + + # Then + kili.kili_api_gateway.graphql_client.execute.assert_called_once_with( + get_update_project_workflow_mutation(" enforceStepSeparation"), + { + "input": {"projectId": "fake_proj_id", "enforceStepSeparation": False}, + }, + ) diff --git a/tests/integration/use_cases/test_project_workflow.py b/tests/integration/use_cases/test_project_workflow.py new file mode 100644 index 000000000..aa9ba0fec --- /dev/null +++ b/tests/integration/use_cases/test_project_workflow.py @@ -0,0 +1,31 @@ +from kili.adapters.kili_api_gateway.kili_api_gateway import KiliAPIGateway +from kili.adapters.kili_api_gateway.project_workflow.types import ( + ProjectWorkflowDataKiliAPIGatewayInput, +) +from kili.domain.project import ProjectId +from kili.use_cases.project_workflow import ProjectWorkflowUseCases + + +def test_given_a_project_workflow_when_update_it_then_it_updates_project_workflow_props( + kili_api_gateway: KiliAPIGateway, +): + # Given + def mocked_update_project_workflow( + project_id: ProjectId, + project_workflow_data: ProjectWorkflowDataKiliAPIGatewayInput, + ): + return { + "enforce_step_separation": project_workflow_data.enforce_step_separation, + "project_id": project_id, + } + + kili_api_gateway.update_project_workflow.side_effect = mocked_update_project_workflow + + # When + project = ProjectWorkflowUseCases(kili_api_gateway).update_project_workflow( + project_id=ProjectId("fake_proj_id"), + enforce_step_separation=False, + ) + + # Then + assert project == {"enforce_step_separation": False, "project_id": "fake_proj_id"}