Skip to content

Commit d6a28a3

Browse files
authored
Merge pull request #2039 from kili-technology/feature/lab-4295-aau-i-add-update-and-delete-steps-with-the-sdk-2
fix(LAB-4295): add differents methods to handle review and labeling steps
2 parents a089f91 + 4da5c2c commit d6a28a3

9 files changed

Lines changed: 713 additions & 5 deletions

File tree

recipes/frame_dicom_data.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@
318318
"cell_type": "markdown",
319319
"metadata": {},
320320
"source": [
321-
"Finally, let's import the volumes using `appendManyToDataset` (see [link](https://staging.cloud.kili-technology.com/docs/python-graphql-api/python-api/#append_many_to_dataset)). The key argument is `json_content_array`, which is a list of list of strings. Each element is the list of urls or paths pointing to images of the volume considered."
321+
"Finally, let's import the volumes using `appendManyToDataset`. The key argument is `json_content_array`, which is a list of list of strings. Each element is the list of urls or paths pointing to images of the volume considered."
322322
]
323323
},
324324
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Project Workflow gateway common."""
2+
3+
import warnings
4+
5+
6+
def get_assignees_to_add_ids(existing_members: list[dict], assignees: list[str]) -> list:
7+
"""Get list of assignees ids to add."""
8+
members_by_email = {m["user"]["email"]: m for m in (existing_members or [])}
9+
assignees_to_add = []
10+
assignees_added = []
11+
assignees_not_added = []
12+
for email in assignees:
13+
member = members_by_email.get(email)
14+
if member and member.get("role") != "LABELER":
15+
user_id = member["user"]["id"]
16+
assignees_to_add.append(user_id)
17+
assignees_added.append(email)
18+
else:
19+
assignees_not_added.append(email)
20+
if assignees_not_added:
21+
warnings.warn(
22+
"These emails were not added (not found or can not review): "
23+
+ ", ".join(assignees_not_added)
24+
)
25+
return assignees_to_add

src/kili/adapters/kili_api_gateway/project_workflow/mappers.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
from kili.domain.project import WorkflowStepCreate, WorkflowStepUpdate
44

5-
from .types import ProjectWorkflowDataKiliAPIGatewayInput
5+
from .types import (
6+
AddReviewStepInput,
7+
DeleteStepInput,
8+
ProjectWorkflowDataKiliAPIGatewayInput,
9+
RenameStepInput,
10+
UpdateLabelingStepPropertiesInput,
11+
UpdateReviewStepPropertiesInput,
12+
)
613

714

815
def project_input_mapper(data: ProjectWorkflowDataKiliAPIGatewayInput) -> dict:
@@ -46,3 +53,68 @@ def update_step_mapper(
4653
for gql_key, py_key in mapping.items()
4754
if py_key in data and (data[py_key] is not None or gql_key in null_fields)
4855
}
56+
57+
58+
def add_review_step_input_mapper(data: AddReviewStepInput) -> dict:
59+
"""Build the GraphQL AddReviewStepInput variable."""
60+
result: dict = {
61+
"projectId": data.project_id,
62+
"name": data.step_name,
63+
"assignees": data.assignees,
64+
}
65+
if data.step_coverage is not None:
66+
result["stepCoverage"] = data.step_coverage
67+
if data.use_honeypot is not None:
68+
result["useHoneypot"] = data.use_honeypot
69+
if data.send_back_to_step is not None:
70+
result["sendBackStepId"] = data.send_back_to_step
71+
return result
72+
73+
74+
def update_labeling_step_properties_input_mapper(data: UpdateLabelingStepPropertiesInput) -> dict:
75+
"""Build the GraphQL UpdateLabelingStepPropertiesInput variable."""
76+
result: dict = {
77+
"projectId": data.project_id,
78+
"stepId": data.step_id,
79+
}
80+
if data.consensus_coverage is not None:
81+
result["consensusCoverage"] = data.consensus_coverage
82+
if data.number_of_expected_labels_for_consensus is not None:
83+
result["numberOfExpectedLabelsForConsensus"] = data.number_of_expected_labels_for_consensus
84+
if data.use_honeypot is not None:
85+
result["useHoneypot"] = data.use_honeypot
86+
return result
87+
88+
89+
def update_review_step_properties_input_mapper(data: UpdateReviewStepPropertiesInput) -> dict:
90+
"""Build the GraphQL UpdateReviewStepPropertiesInput variable."""
91+
result: dict = {
92+
"projectId": data.project_id,
93+
"stepId": data.step_id,
94+
}
95+
if data.assignees is not None:
96+
result["assignees"] = data.assignees
97+
if data.step_coverage is not None:
98+
result["stepCoverage"] = data.step_coverage
99+
if data.send_back_to_step is not None:
100+
result["sendBackStepId"] = data.send_back_to_step
101+
if data.use_honeypot is not None:
102+
result["useHoneypot"] = data.use_honeypot
103+
return result
104+
105+
106+
def delete_step_input_mapper(data: DeleteStepInput) -> dict:
107+
"""Build the GraphQL DeleteStepInput variable."""
108+
return {
109+
"projectId": data.project_id,
110+
"stepId": data.step_id,
111+
}
112+
113+
114+
def rename_step_input_mapper(data: RenameStepInput) -> dict:
115+
"""Build the GraphQL RenameStepInput variable."""
116+
return {
117+
"projectId": data.project_id,
118+
"stepId": data.step_id,
119+
"newName": data.new_name,
120+
}

src/kili/adapters/kili_api_gateway/project_workflow/operations.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,58 @@ def get_steps_query(fragment: str) -> str:
2121
}}
2222
}}
2323
"""
24+
25+
26+
def get_add_review_step_mutation() -> str:
27+
"""Return the GraphQL addReviewStep mutation."""
28+
return """
29+
mutation addReviewStep($input: AddReviewStepInput!) {
30+
data: addReviewStep(input: $input) {
31+
steps{id,name}
32+
}
33+
}
34+
"""
35+
36+
37+
def get_update_labeling_step_properties_mutation() -> str:
38+
"""Return the GraphQL updateLabelingStepProperties mutation."""
39+
return """
40+
mutation updateLabelingStepProperties($input: UpdateLabelingStepPropertiesInput!) {
41+
data: updateLabelingStepProperties(input: $input) {
42+
steps{id,name}
43+
}
44+
}
45+
"""
46+
47+
48+
def get_update_review_step_properties_mutation() -> str:
49+
"""Return the GraphQL updateReviewStepProperties mutation."""
50+
return """
51+
mutation updateReviewStepProperties($input: UpdateReviewStepPropertiesInput!) {
52+
data: updateReviewStepProperties(input: $input) {
53+
steps{id,name}
54+
}
55+
}
56+
"""
57+
58+
59+
def get_delete_step_mutation() -> str:
60+
"""Return the GraphQL deleteStep mutation."""
61+
return """
62+
mutation deleteStep($input: DeleteStepInput!) {
63+
data: deleteStep(input: $input) {
64+
steps{id,name}
65+
}
66+
}
67+
"""
68+
69+
70+
def get_rename_step_mutation() -> str:
71+
"""Return the GraphQL renameStep mutation."""
72+
return """
73+
mutation renameStep($input: RenameStepInput!) {
74+
data: renameStep(input: $input) {
75+
steps{id,name}
76+
}
77+
}
78+
"""

src/kili/adapters/kili_api_gateway/project_workflow/operations_mixin.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,32 @@
1212
from kili.domain.types import ListOrTuple
1313
from kili.exceptions import NotFound
1414

15-
from .mappers import project_input_mapper
15+
from .common import get_assignees_to_add_ids
16+
from .mappers import (
17+
add_review_step_input_mapper,
18+
delete_step_input_mapper,
19+
project_input_mapper,
20+
rename_step_input_mapper,
21+
update_labeling_step_properties_input_mapper,
22+
update_review_step_properties_input_mapper,
23+
)
1624
from .operations import (
25+
get_add_review_step_mutation,
26+
get_delete_step_mutation,
27+
get_rename_step_mutation,
1728
get_steps_query,
29+
get_update_labeling_step_properties_mutation,
1830
get_update_project_workflow_mutation,
31+
get_update_review_step_properties_mutation,
32+
)
33+
from .types import (
34+
AddReviewStepInput,
35+
DeleteStepInput,
36+
ProjectWorkflowDataKiliAPIGatewayInput,
37+
RenameStepInput,
38+
UpdateLabelingStepPropertiesInput,
39+
UpdateReviewStepPropertiesInput,
1940
)
20-
from .types import ProjectWorkflowDataKiliAPIGatewayInput
2141

2242

2343
class ProjectWorkflowOperationMixin(BaseOperationMixin):
@@ -72,7 +92,7 @@ def list_activated_project_users(self, project_id: str) -> list[dict]:
7292
return list(
7393
ProjectUserQuery(self.graphql_client, self.http_client)(
7494
where=where,
75-
fields=["role", "user.id"],
95+
fields=["role", "user.id", "user.email"],
7696
options=QueryOptions(disable_tqdm=True),
7797
)
7898
)
@@ -177,3 +197,61 @@ def remove_reviewers_from_step(
177197
)
178198

179199
return removed_emails
200+
201+
def add_review_step(self, data: AddReviewStepInput) -> dict:
202+
"""Add a review step to a project workflow."""
203+
existing_members = self.list_activated_project_users(data.project_id)
204+
assignees_to_add = get_assignees_to_add_ids(existing_members, data.assignees)
205+
data.assignees = assignees_to_add
206+
variables = {"input": add_review_step_input_mapper(data)}
207+
mutation = get_add_review_step_mutation()
208+
result = self.graphql_client.execute(mutation, variables)
209+
steps = result.get("data", {}).get("steps", [])
210+
step = next((step for step in steps if step.get("name") == data.step_name), None)
211+
if not step:
212+
raise NotFound(f"Could not find the stepId of the step {data.step_name}.")
213+
return step
214+
215+
def update_labeling_step_properties(self, data: UpdateLabelingStepPropertiesInput) -> dict:
216+
"""Update properties of a labeling step."""
217+
variables = {"input": update_labeling_step_properties_input_mapper(data)}
218+
mutation = get_update_labeling_step_properties_mutation()
219+
result = self.graphql_client.execute(mutation, variables)
220+
steps = result.get("data", {}).get("steps", [])
221+
step = next((step for step in steps if step.get("id") == data.step_id), None)
222+
if not step:
223+
raise NotFound(f"Could not find the step with id {data.step_id}.")
224+
return step
225+
226+
def update_review_step_properties(self, data: UpdateReviewStepPropertiesInput) -> dict:
227+
"""Update properties of a review step."""
228+
if data.assignees is not None:
229+
existing_members = self.list_activated_project_users(data.project_id)
230+
assignees_to_add = get_assignees_to_add_ids(existing_members, data.assignees)
231+
data.assignees = assignees_to_add
232+
variables = {"input": update_review_step_properties_input_mapper(data)}
233+
mutation = get_update_review_step_properties_mutation()
234+
result = self.graphql_client.execute(mutation, variables)
235+
steps = result.get("data", {}).get("steps", [])
236+
step = next((step for step in steps if step.get("id") == data.step_id), None)
237+
if not step:
238+
raise NotFound(f"Could not find the step with id {data.step_id}.")
239+
return step
240+
241+
def delete_step(self, data: DeleteStepInput) -> dict:
242+
"""Delete a step from a project workflow."""
243+
variables = {"input": delete_step_input_mapper(data)}
244+
mutation = get_delete_step_mutation()
245+
result = self.graphql_client.execute(mutation, variables)
246+
return result["data"]
247+
248+
def rename_step(self, data: RenameStepInput) -> dict:
249+
"""Rename a step in a project workflow."""
250+
variables = {"input": rename_step_input_mapper(data)}
251+
mutation = get_rename_step_mutation()
252+
result = self.graphql_client.execute(mutation, variables)
253+
steps = result.get("data", {}).get("steps", [])
254+
step = next((step for step in steps if step.get("id") == data.step_id), None)
255+
if not step:
256+
raise NotFound(f"Could not find the step with id {data.step_id}.")
257+
return step

src/kili/adapters/kili_api_gateway/project_workflow/types.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,55 @@ class ProjectWorkflowDataKiliAPIGatewayInput:
1515
update_steps: Optional[list[WorkflowStepUpdate]]
1616
delete_steps: Optional[list[str]]
1717
null_fields: frozenset[str] = frozenset()
18+
19+
20+
@dataclass
21+
class AddReviewStepInput:
22+
"""Input data for adding a review step to a project workflow."""
23+
24+
project_id: str
25+
step_name: str
26+
assignees: list[str]
27+
step_coverage: int | None = None
28+
use_honeypot: bool | None = None
29+
send_back_to_step: str | None = None
30+
31+
32+
@dataclass
33+
class UpdateLabelingStepPropertiesInput:
34+
"""Input data for updating labeling step properties."""
35+
36+
project_id: str
37+
step_id: str
38+
consensus_coverage: int | None = None
39+
number_of_expected_labels_for_consensus: int | None = None
40+
use_honeypot: bool | None = None
41+
42+
43+
@dataclass
44+
class UpdateReviewStepPropertiesInput:
45+
"""Input data for updating review step properties."""
46+
47+
project_id: str
48+
step_id: str
49+
assignees: list[str] | None = None
50+
step_coverage: int | None = None
51+
send_back_to_step: str | None = None
52+
use_honeypot: bool | None = None
53+
54+
55+
@dataclass
56+
class DeleteStepInput:
57+
"""Input data for deleting a step from a project workflow."""
58+
59+
project_id: str
60+
step_id: str
61+
62+
63+
@dataclass
64+
class RenameStepInput:
65+
"""Input data for renaming a step in a project workflow."""
66+
67+
project_id: str
68+
step_id: str
69+
new_name: str

0 commit comments

Comments
 (0)