Skip to content

Commit a5cc12a

Browse files
feat(LAB-3534): add stepIdIn and stepStatusIn filters (#1885)
1 parent 2072a72 commit a5cc12a

File tree

8 files changed

+208
-18
lines changed

8 files changed

+208
-18
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,6 @@ def asset_where_mapper(filters: AssetFilters):
6464
"type": filters.issue_type,
6565
"status": filters.issue_status,
6666
},
67+
"stepIdIn": filters.step_id_in,
68+
"stepStatusIn": filters.step_status_in,
6769
}

src/kili/domain/asset/asset.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212

1313
AssetId = NewType("AssetId", str)
1414
AssetExternalId = NewType("AssetExternalId", str)
15-
15+
AssetStatusInStep = NewType("AssetStatusInStep", str)
1616

1717
AssetStatus = Literal["TODO", "ONGOING", "LABELED", "REVIEWED", "TO_REVIEW"]
1818

19+
StatusInStep = Literal["TO_DO", "DOING", "PARTIALLY_DONE", "REDO", "DONE", "SKIPPED"]
20+
1921

2022
@dataclass
2123
class AssetFilters:
@@ -48,8 +50,6 @@ class AssetFilters:
4850
assignee_in: Optional[ListOrTuple[str]] = None
4951
assignee_not_in: Optional[ListOrTuple[str]] = None
5052
metadata_where: Optional[dict] = None
51-
skipped: Optional[bool] = None
52-
status_in: Optional[ListOrTuple[AssetStatus]] = None
5353
updated_at_gte: Optional[str] = None
5454
updated_at_lte: Optional[str] = None
5555
label_category_search: Optional[str] = None
@@ -59,3 +59,7 @@ class AssetFilters:
5959
inference_mark_lte: Optional[float] = None
6060
issue_type: Optional["IssueType"] = None
6161
issue_status: Optional["IssueStatus"] = None
62+
skipped: Optional[bool] = None
63+
status_in: Optional[ListOrTuple[AssetStatus]] = None
64+
step_id_in: Optional[ListOrTuple[str]] = None
65+
step_status_in: Optional[ListOrTuple[StatusInStep]] = None

src/kili/domain/project.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from dataclasses import dataclass
44
from enum import Enum
5-
from typing import TYPE_CHECKING, Literal, NewType, Optional
5+
from typing import TYPE_CHECKING, Literal, NewType, Optional, TypedDict
66

77
from .types import ListOrTuple
88

@@ -15,6 +15,14 @@
1515
]
1616

1717

18+
@dataclass(frozen=True)
19+
class ProjectStep(TypedDict, total=True):
20+
"""Project step type."""
21+
22+
id: str
23+
name: str
24+
25+
1826
class InputTypeEnum(str, Enum):
1927
"""Input type enum."""
2028

src/kili/presentation/client/asset.py

+61-10
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,25 @@
1717
from typeguard import typechecked
1818

1919
from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions
20-
from kili.domain.asset import AssetExternalId, AssetFilters, AssetId, AssetStatus
20+
from kili.domain.asset.asset import (
21+
AssetExternalId,
22+
AssetFilters,
23+
AssetId,
24+
AssetStatus,
25+
StatusInStep,
26+
)
2127
from kili.domain.issue import IssueStatus, IssueType
2228
from kili.domain.label import LabelType
2329
from kili.domain.project import ProjectId
2430
from kili.domain.types import ListOrTuple
2531
from kili.presentation.client.helpers.common_validators import (
2632
disable_tqdm_if_as_generator,
2733
)
34+
from kili.presentation.client.helpers.filter_conversion import (
35+
convert_step_in_to_step_id_in_filter,
36+
)
2837
from kili.use_cases.asset import AssetUseCases
38+
from kili.use_cases.project.project import ProjectUseCases
2939
from kili.utils.logcontext import for_all_methods, log_call
3040

3141
from .base import BaseClientMethods
@@ -80,8 +90,6 @@ def assets(
8090
label_honeypot_mark_lt: Optional[float] = None,
8191
label_type_in: Optional[List[LabelType]] = None,
8292
metadata_where: Optional[dict] = None,
83-
skipped: Optional[bool] = None,
84-
status_in: Optional[List[AssetStatus]] = None,
8593
updated_at_gte: Optional[str] = None,
8694
updated_at_lte: Optional[str] = None,
8795
label_category_search: Optional[str] = None,
@@ -112,6 +120,10 @@ def assets(
112120
external_id_strictly_in: Optional[List[str]] = None,
113121
external_id_in: Optional[List[str]] = None,
114122
label_output_format: Literal["dict", "parsed_label"] = "dict",
123+
skipped: Optional[bool] = None,
124+
status_in: Optional[List[AssetStatus]] = None,
125+
step_name_in: Optional[List[str]] = None,
126+
step_status_in: Optional[List[StatusInStep]] = None,
115127
*,
116128
as_generator: Literal[True],
117129
) -> Generator[Dict, None, None]:
@@ -158,8 +170,6 @@ def assets(
158170
label_honeypot_mark_lt: Optional[float] = None,
159171
label_type_in: Optional[List[LabelType]] = None,
160172
metadata_where: Optional[dict] = None,
161-
skipped: Optional[bool] = None,
162-
status_in: Optional[List[AssetStatus]] = None,
163173
updated_at_gte: Optional[str] = None,
164174
updated_at_lte: Optional[str] = None,
165175
label_category_search: Optional[str] = None,
@@ -190,6 +200,10 @@ def assets(
190200
external_id_strictly_in: Optional[List[str]] = None,
191201
external_id_in: Optional[List[str]] = None,
192202
label_output_format: Literal["dict", "parsed_label"] = "dict",
203+
skipped: Optional[bool] = None,
204+
status_in: Optional[List[AssetStatus]] = None,
205+
step_name_in: Optional[List[str]] = None,
206+
step_status_in: Optional[List[StatusInStep]] = None,
193207
*,
194208
as_generator: Literal[False] = False,
195209
) -> List[Dict]:
@@ -236,8 +250,6 @@ def assets(
236250
label_honeypot_mark_lt: Optional[float] = None,
237251
label_type_in: Optional[List[LabelType]] = None,
238252
metadata_where: Optional[dict] = None,
239-
skipped: Optional[bool] = None,
240-
status_in: Optional[List[AssetStatus]] = None,
241253
updated_at_gte: Optional[str] = None,
242254
updated_at_lte: Optional[str] = None,
243255
label_category_search: Optional[str] = None,
@@ -268,6 +280,10 @@ def assets(
268280
external_id_strictly_in: Optional[List[str]] = None,
269281
external_id_in: Optional[List[str]] = None,
270282
label_output_format: Literal["dict", "parsed_label"] = "dict",
283+
skipped: Optional[bool] = None,
284+
status_in: Optional[List[AssetStatus]] = None,
285+
step_name_in: Optional[List[str]] = None,
286+
step_status_in: Optional[List[StatusInStep]] = None,
271287
*,
272288
as_generator: bool = False,
273289
) -> Union[Iterable[Dict], "pd.DataFrame"]:
@@ -289,8 +305,6 @@ def assets(
289305
metadata_where: Filters by the values of the metadata of the asset.
290306
honeypot_mark_gt: Deprecated. Use `honeypot_mark_gte` instead.
291307
honeypot_mark_lt: Deprecated. Use `honeypot_mark_lte` instead.
292-
status_in: Returned assets should have a status that belongs to that list, if given.
293-
Possible choices: `TODO`, `ONGOING`, `LABELED`, `TO_REVIEW` or `REVIEWED`.
294308
label_type_in: Returned assets should have a label whose type belongs to that list, if given.
295309
label_author_in: Returned assets should have a label whose author belongs to that list, if given. An author can be designated by the first name, the last name, or the first name + last name.
296310
label_consensus_mark_gt: Deprecated. Use `label_consensus_mark_gte` instead.
@@ -300,7 +314,6 @@ def assets(
300314
label_created_at_lt: Deprecated. Use `label_created_at_lte` instead.
301315
label_honeypot_mark_gt: Deprecated. Use `label_honeypot_mark_gte` instead.
302316
label_honeypot_mark_lt: Deprecated. Use `label_honeypot_mark_lte` instead.
303-
skipped: Returned assets should be skipped
304317
updated_at_gte: Returned assets should have a label whose update date is greater or equal to this date.
305318
updated_at_lte: Returned assets should have a label whose update date is lower or equal to this date.
306319
format: If equal to 'pandas', returns a pandas DataFrame
@@ -335,6 +348,15 @@ def assets(
335348
external_id_in: Returned assets should have external ids that partially match the ones in the list.
336349
For example, with `external_id_in=['abc']`, any asset with an external id containing `'abc'` will be returned.
337350
label_output_format: If `parsed_label`, the labels in the assets will be parsed. More information on parsed labels in the [documentation](https://python-sdk-docs.kili-technology.com/latest/sdk/tutorials/label_parsing/).
351+
skipped: Returned assets should be skipped
352+
Only applicable if the project is in WorkflowV1 (legacy).
353+
status_in: Returned assets should have a status that belongs to that list, if given.
354+
Possible choices: `TODO`, `ONGOING`, `LABELED`, `TO_REVIEW` or `REVIEWED`.
355+
Only applicable if the project is in the WorkflowV1 (legacy).
356+
step_name_in: Returned assets are in the step whose name belong to that list, if given.
357+
Only applicable if the project is in WorkflowV2.
358+
step_status_in: Returned assets have the status in their step that belongs to that list, if given.
359+
Only applicable if the project is in WorkflowV2.
338360
339361
!!! info "Dates format"
340362
Date strings should have format: "YYYY-MM-DD"
@@ -431,6 +453,33 @@ def assets(
431453

432454
disable_tqdm = disable_tqdm_if_as_generator(as_generator, disable_tqdm)
433455

456+
step_id_in = None
457+
if (
458+
step_name_in is not None
459+
or step_status_in is not None
460+
or status_in is not None
461+
or skipped is not None
462+
):
463+
project_use_cases = ProjectUseCases(self.kili_api_gateway)
464+
project_steps = project_use_cases.get_project_steps(project_id)
465+
466+
if step_name_in is not None or step_status_in is not None or status_in is not None:
467+
step_id_in = convert_step_in_to_step_id_in_filter(
468+
project_steps=project_steps,
469+
fields=fields,
470+
asset_filter_kwargs={
471+
"step_name_in": step_name_in,
472+
"step_status_in": step_status_in,
473+
"status_in": status_in,
474+
"skipped": skipped,
475+
},
476+
)
477+
elif skipped is not None and len(project_steps) != 0:
478+
warnings.warn(
479+
"Filter skipped given : only use filter step_status_in with the SKIPPED step status instead for this project",
480+
stacklevel=1,
481+
)
482+
434483
asset_use_cases = AssetUseCases(self.kili_api_gateway)
435484
filters = AssetFilters(
436485
project_id=ProjectId(project_id),
@@ -474,6 +523,8 @@ def assets(
474523
assignee_not_in=assignee_not_in,
475524
issue_status=issue_status,
476525
issue_type=issue_type,
526+
step_id_in=step_id_in,
527+
step_status_in=step_status_in,
477528
)
478529
assets_gen = asset_use_cases.list_assets(
479530
filters,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Module for common argument validators across client methods."""
2+
3+
import warnings
4+
from typing import Dict, List, Optional
5+
6+
from kili.domain.project import ProjectStep
7+
from kili.domain.types import ListOrTuple
8+
9+
10+
def extract_step_ids_from_project_steps(
11+
project_steps: List[ProjectStep],
12+
step_name_in: List[str],
13+
) -> List[str]:
14+
"""Extract step ids from project steps."""
15+
matching_steps = [step for step in project_steps if step["name"] in step_name_in]
16+
17+
# Raise an exception if any name in step_name_in does not match a step["name"]
18+
unmatched_names = [
19+
name for name in step_name_in if name not in [step["name"] for step in project_steps]
20+
]
21+
if unmatched_names:
22+
raise ValueError(f"The following step names do not match any steps: {unmatched_names}")
23+
24+
return [step["id"] for step in matching_steps]
25+
26+
27+
def convert_step_in_to_step_id_in_filter(
28+
asset_filter_kwargs: Dict[str, object],
29+
project_steps: List[ProjectStep],
30+
fields: Optional[ListOrTuple[str]] = None,
31+
) -> Optional[List[str]]:
32+
"""If a stepIn filter is given, convert it to a stepIdIn and return it."""
33+
step_name_in = asset_filter_kwargs.get("step_name_in")
34+
step_status_in = asset_filter_kwargs.get("step_status_in")
35+
status_in = asset_filter_kwargs.get("status_in")
36+
skipped = asset_filter_kwargs.get("skipped")
37+
38+
if len(project_steps) != 0:
39+
if step_status_in is not None and status_in is not None:
40+
raise ValueError(
41+
"Filters step_status_in and status_in both given : only use filter step_status_in for this project."
42+
)
43+
if step_name_in is not None and status_in is not None:
44+
raise ValueError(
45+
"Filters step_name_in and status_in both given : use filter step_status_in instead of status_in for this project." # pylint: disable=line-too-long
46+
)
47+
if status_in is not None:
48+
warnings.warn(
49+
"Filter status_in given : use filters step_status_in and step_name_in instead for this project.",
50+
stacklevel=1,
51+
)
52+
if skipped is not None:
53+
warnings.warn(
54+
"Filter skipped given : only use filter step_status_in with the SKIPPED step status instead for this project", # pylint: disable=line-too-long
55+
stacklevel=1,
56+
)
57+
if fields and "status" in fields:
58+
warnings.warn(
59+
"Field status requested : request fields step and stepStatus instead for this project",
60+
stacklevel=1,
61+
)
62+
63+
if (
64+
step_name_in is not None
65+
and isinstance(step_name_in, list)
66+
and all(isinstance(item, str) for item in step_name_in)
67+
):
68+
return extract_step_ids_from_project_steps(
69+
project_steps=project_steps, step_name_in=step_name_in
70+
)
71+
return None
72+
73+
if step_name_in is not None or step_status_in is not None:
74+
raise ValueError(
75+
"Filters step_name_in and/or step_status_in given : use filter status_in for this project."
76+
)
77+
return None

src/kili/presentation/client/label.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,16 @@
3434
assert_all_arrays_have_same_size,
3535
disable_tqdm_if_as_generator,
3636
)
37+
from kili.presentation.client.helpers.filter_conversion import (
38+
convert_step_in_to_step_id_in_filter,
39+
)
3740
from kili.services.export import export_labels
3841
from kili.services.export.exceptions import NoCompatibleJobError
3942
from kili.services.export.types import CocoAnnotationModifier, LabelFormat, SplitOption
4043
from kili.use_cases.asset.utils import AssetUseCasesUtils
4144
from kili.use_cases.label import LabelUseCases
4245
from kili.use_cases.label.types import LabelToCreateUseCaseInput
46+
from kili.use_cases.project.project import ProjectUseCases
4347
from kili.utils.labels.parsing import ParsedLabel
4448
from kili.utils.logcontext import for_all_methods, log_call
4549

@@ -1146,8 +1150,8 @@ def export_labels(
11461150
- `label_reviewer_not_in`
11471151
- `assignee_in`
11481152
- `assignee_not_in`
1149-
- `skipped`
1150-
- `status_in`
1153+
- `skipped`: only applicable if the project is in the WorkflowV1 (legacy).
1154+
- `status_in`: only applicable if the project is in the WorkflowV1 (legacy).
11511155
- `label_category_search`
11521156
- `created_at_gte`
11531157
- `created_at_lte`
@@ -1156,6 +1160,8 @@ def export_labels(
11561160
- `inference_mark_gte`
11571161
- `inference_mark_lte`
11581162
- `metadata_where`
1163+
- `step_name_in`: : only applicable if the project is in the WorkflowV2.
1164+
- `step_status_in`: only applicable if the project is in the WorkflowV2.
11591165
11601166
See the documentation of [`kili.assets()`](https://python-sdk-docs.kili-technology.com/latest/sdk/asset/#kili.queries.asset.__init__.QueriesAsset.assets) for more information.
11611167
normalized_coordinates: This parameter is only effective on the Kili (a.k.a raw) format.
@@ -1204,6 +1210,31 @@ def is_rectangle(coco_annotation, coco_image, kili_annotation):
12041210
else:
12051211
resolved_asset_ids = cast(List[AssetId], asset_ids)
12061212

1213+
if asset_filter_kwargs and (
1214+
asset_filter_kwargs.get("step_name_in") is not None
1215+
or asset_filter_kwargs.get("step_status_in") is not None
1216+
or asset_filter_kwargs.get("status_in") is not None
1217+
or asset_filter_kwargs.get("skipped") is not None
1218+
):
1219+
project_use_cases = ProjectUseCases(self.kili_api_gateway)
1220+
project_steps = project_use_cases.get_project_steps(project_id)
1221+
1222+
step_name_in = asset_filter_kwargs.get("step_name_in")
1223+
step_status_in = asset_filter_kwargs.get("step_status_in")
1224+
status_in = asset_filter_kwargs.get("status_in")
1225+
skipped = asset_filter_kwargs.get("skipped")
1226+
if step_name_in is not None or step_status_in is not None or status_in is not None:
1227+
step_id_in = convert_step_in_to_step_id_in_filter(
1228+
project_steps=project_steps, asset_filter_kwargs=asset_filter_kwargs
1229+
)
1230+
asset_filter_kwargs.pop("step_name_in", None)
1231+
asset_filter_kwargs["step_id_in"] = step_id_in
1232+
elif skipped is not None and len(project_steps) != 0:
1233+
warnings.warn(
1234+
"Filter skipped given : only use filter step_status_in with the SKIPPED step status instead for this project",
1235+
stacklevel=1,
1236+
)
1237+
12071238
try:
12081239
return export_labels(
12091240
self, # pyright: ignore[reportGeneralTypeIssues]

src/kili/services/export/tools.py

+2
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ def fetch_assets(
140140
"inference_mark_gte": asset_filter_kwargs.pop("inference_mark_gte", None),
141141
"inference_mark_lte": asset_filter_kwargs.pop("inference_mark_lte", None),
142142
"metadata_where": asset_filter_kwargs.pop("metadata_where", None),
143+
"step_id_in": asset_filter_kwargs.pop("step_id_in", None),
144+
"step_status_in": asset_filter_kwargs.pop("step_status_in", None),
143145
}
144146

145147
if asset_filter_kwargs:

0 commit comments

Comments
 (0)