Skip to content

Commit bf30f08

Browse files
authored
Vb/search projects filters plt 1399 (#1771)
1 parent 27c77b9 commit bf30f08

File tree

11 files changed

+411
-30
lines changed

11 files changed

+411
-30
lines changed

docs/labelbox/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Labelbox Python SDK Documentation
3030
labeling-frontend-options
3131
labeling-parameter-override
3232
labeling-service
33+
labeling-service-dashboard
3334
model
3435
model-config
3536
model-run
@@ -42,6 +43,7 @@ Labelbox Python SDK Documentation
4243
quality-mode
4344
resource-tag
4445
review
46+
search-filters
4547
send-to-annotate-params
4648
slice
4749
task
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Labeling Service Dashboard
2+
===============================================================================================
3+
4+
.. automodule:: labelbox.schema.labeling_service_dashboard
5+
:show-inheritance:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Labeling Service Status
2+
===============================================================================================
3+
4+
.. automodule:: labelbox.schema.labeling_service_status
5+
:members:
6+
:show-inheritance:

docs/labelbox/search-filters.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Search Filters
2+
===============================================================================================
3+
4+
.. automodule:: labelbox.schema.search_filters
5+
:members:
6+
:exclude-members: _dict_to_graphql_string
7+
:show-inheritance:

libs/labelbox/src/labelbox/client.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from datetime import datetime, timezone
1212
from typing import Any, List, Dict, Union, Optional, overload, Callable
1313

14+
from labelbox.schema.search_filters import SearchFilter
1415
import requests
1516
import requests.exceptions
1617
from google.api_core import retry
@@ -2409,21 +2410,38 @@ def upsert_label_feedback(self, label_id: str, feedback: str,
24092410

24102411
def get_labeling_service_dashboards(
24112412
self,
2412-
after: Optional[str] = None,
2413-
search_query: Optional[List[Dict]] = None,
2413+
search_query: Optional[List[SearchFilter]] = None,
24142414
) -> PaginatedCollection:
24152415
"""
24162416
Get all labeling service dashboards for a given org.
24172417
24182418
Optional parameters:
2419-
after: The cursor to use for pagination.
2420-
where: A filter to apply to the query.
2421-
2422-
NOTE: support for after and search_query are not yet implemented.
2423-
"""
2424-
return LabelingServiceDashboard.get_all(self,
2425-
after,
2426-
search_query=search_query)
2419+
search_query: A list of search filters representing the search
2420+
2421+
NOTE:
2422+
- Retrieves all projects for the organization or as filtered by the search query.
2423+
- Sorted by project created date in ascending order.
2424+
2425+
Examples:
2426+
Retrieves all labeling service dashboards for a given workspace id:
2427+
>>> workspace_filter = WorkspaceFilter(
2428+
>>> operation=OperationType.Workspace,
2429+
>>> operator=IdOperator.Is,
2430+
>>> values=[workspace_id])
2431+
>>> labeling_service_dashboard = [
2432+
>>> ld for ld in project.client.get_labeling_service_dashboards(search_query=[workspace_filter])]
2433+
2434+
Retrieves all labeling service dashboards requested less than 7 days ago:
2435+
>>> seven_days_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
2436+
>>> workforce_requested_filter_before = WorkforceRequestedDateFilter(
2437+
>>> operation=OperationType.WorforceRequestedDate,
2438+
>>> value=DateValue(operator=DateOperator.GreaterThanOrEqual,
2439+
>>> value=seven_days_ago))
2440+
>>> labeling_service_dashboard = [ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_requested_filter_before])]
2441+
2442+
See libs/labelbox/src/labelbox/schema/search_filters.py and libs/labelbox/tests/unit/test_unit_search_filters.py for more examples.
2443+
"""
2444+
return LabelingServiceDashboard.get_all(self, search_query=search_query)
24272445

24282446
def get_task_by_id(self, task_id: str) -> Union[Task, DataUpsertTask]:
24292447
"""

libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from labelbox.exceptions import ResourceNotFoundError
66
from labelbox.pagination import PaginatedCollection
77
from labelbox.pydantic_compat import BaseModel, root_validator, Field
8+
from labelbox.schema.search_filters import SearchFilter, build_search_filter
89
from labelbox.utils import _CamelCaseMixin
910
from labelbox.schema.labeling_service_status import LabelingServiceStatus
1011

@@ -94,28 +95,36 @@ def get(cls, client, project_id: str) -> 'LabelingServiceDashboard':
9495
def get_all(
9596
cls,
9697
client,
97-
after: Optional[str] = None,
98-
search_query: Optional[List[Dict]] = None,
98+
search_query: Optional[List[SearchFilter]] = None,
9999
) -> PaginatedCollection:
100-
template = Template(
101-
"""query SearchProjectsPyApi($$first: Int, $$from: String) {
102-
searchProjects(input: {after: $$from, searchQuery: $search_query, size: $$first})
103-
{
104-
nodes { $labeling_dashboard_selections }
105-
pageInfo { endCursor }
100+
101+
if search_query is not None:
102+
template = Template(
103+
"""query SearchProjectsPyApi($$first: Int, $$from: String) {
104+
searchProjects(input: {after: $$from, searchQuery: $search_query, size: $$first})
105+
{
106+
nodes { $labeling_dashboard_selections }
107+
pageInfo { endCursor }
108+
}
109+
}
110+
""")
111+
else:
112+
template = Template(
113+
"""query SearchProjectsPyApi($$first: Int, $$from: String) {
114+
searchProjects(input: {after: $$from, size: $$first})
115+
{
116+
nodes { $labeling_dashboard_selections }
117+
pageInfo { endCursor }
118+
}
106119
}
107-
}
108-
""")
109-
organization_id = client.get_organization().uid
120+
""")
110121
query_str = template.substitute(
111122
labeling_dashboard_selections=GRAPHQL_QUERY_SELECTIONS,
112-
search_query=
113-
f"[{{type: \"organization\", operator: \"is\", values: [\"{organization_id}\"]}}]"
123+
search_query=build_search_filter(search_query)
124+
if search_query else None,
114125
)
115126

116127
params: Dict[str, Union[str, int]] = {}
117-
if after:
118-
params = {"from": after}
119128

120129
def convert_to_labeling_service_dashboard(client, data):
121130
data['client'] = client
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import datetime
2+
from enum import Enum
3+
from typing import List, Literal, Union
4+
5+
from labelbox.pydantic_compat import BaseModel
6+
from labelbox.utils import format_iso_datetime
7+
8+
9+
class BaseSearchFilter(BaseModel):
10+
"""
11+
Shared code for all search filters
12+
"""
13+
14+
class Config:
15+
use_enum_values = True
16+
17+
def dict(self, *args, **kwargs):
18+
res = super().dict(*args, **kwargs)
19+
if 'operation' in res:
20+
res['type'] = res.pop('operation')
21+
22+
# go through all the keys and convert date to string
23+
for key in res:
24+
if isinstance(res[key], datetime.datetime):
25+
res[key] = format_iso_datetime(res[key])
26+
return res
27+
28+
29+
class OperationType(Enum):
30+
"""
31+
Supported search entity types
32+
"""
33+
Organization = 'organization'
34+
Workspace = 'workspace'
35+
Tag = 'tag'
36+
Stage = 'stage'
37+
WorforceRequestedDate = 'workforce_requested_at'
38+
WorkforceStageUpdatedDate = 'workforce_stage_updated_at'
39+
40+
41+
class IdOperator(Enum):
42+
"""
43+
Supported operators for ids
44+
"""
45+
Is = 'is'
46+
47+
48+
class DateOperator(Enum):
49+
"""
50+
Supported operators for dates
51+
"""
52+
Equals = 'EQUALS'
53+
GreaterThanOrEqual = 'GREATER_THAN_OR_EQUAL'
54+
LessThanOrEqual = 'LESS_THAN_OR_EQUAL'
55+
56+
57+
class DateRangeOperator(Enum):
58+
"""
59+
Supported operators for date ranges
60+
"""
61+
Between = 'BETWEEN'
62+
63+
64+
class OrganizationFilter(BaseSearchFilter):
65+
"""
66+
Filter for organization
67+
"""
68+
operation: Literal[OperationType.Organization]
69+
operator: IdOperator
70+
values: List[str]
71+
72+
73+
class WorkspaceFilter(BaseSearchFilter):
74+
"""
75+
Filter for workspace
76+
"""
77+
operation: Literal[OperationType.Workspace]
78+
operator: IdOperator
79+
values: List[str]
80+
81+
82+
class TagFilter(BaseSearchFilter):
83+
"""
84+
Filter for project tags
85+
"""
86+
operation: Literal[OperationType.Tag]
87+
operator: IdOperator
88+
values: List[str]
89+
90+
91+
class ProjectStageFilter(BaseSearchFilter):
92+
"""
93+
Filter labelbox service / aka project stages
94+
"""
95+
operation: Literal[OperationType.Stage]
96+
operator: IdOperator
97+
values: List[str]
98+
99+
100+
class DateValue(BaseSearchFilter):
101+
"""
102+
Date value for a search filter
103+
104+
Date formats:
105+
datetime: an existing datetime object
106+
str the following formats are accepted: YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM]
107+
NOTE
108+
if a date / datetime string is passed without a timezone, we will assume the time is UTC and convert it to a local timezone
109+
so for a string '2024-01-01' that is run on a computer in PST, we would convert it to '2024-01-01T08:00:00Z'
110+
while the same string in EST will get converted to '2024-01-01T05:00:00Z'
111+
"""
112+
operator: DateOperator
113+
value: datetime.datetime
114+
115+
116+
class WorkforceStageUpdatedFilter(BaseSearchFilter):
117+
"""
118+
Filter for workforce stage updated date
119+
"""
120+
operation: Literal[OperationType.WorkforceStageUpdatedDate]
121+
value: DateValue
122+
123+
124+
class WorkforceRequestedDateFilter(BaseSearchFilter):
125+
"""
126+
Filter for workforce requested date
127+
"""
128+
operation: Literal[OperationType.WorforceRequestedDate]
129+
value: DateValue
130+
131+
132+
class DateRange(BaseSearchFilter):
133+
"""
134+
Date range for a search filter
135+
"""
136+
min: datetime.datetime
137+
max: datetime.datetime
138+
139+
140+
class DateRangeValue(BaseSearchFilter):
141+
"""
142+
Date range value for a search filter
143+
"""
144+
operator: DateRangeOperator
145+
value: DateRange
146+
147+
148+
class WorkforceRequestedDateRangeFilter(BaseSearchFilter):
149+
"""
150+
Filter for workforce requested date range
151+
"""
152+
operation: Literal[OperationType.WorforceRequestedDate]
153+
value: DateRangeValue
154+
155+
156+
class WorkforceStageUpdatedRangeFilter(BaseSearchFilter):
157+
"""
158+
Filter for workforce stage updated date range
159+
"""
160+
operation: Literal[OperationType.WorkforceStageUpdatedDate]
161+
value: DateRangeValue
162+
163+
164+
SearchFilter = Union[OrganizationFilter, WorkspaceFilter, TagFilter,
165+
ProjectStageFilter, WorkforceRequestedDateFilter,
166+
WorkforceStageUpdatedFilter,
167+
WorkforceRequestedDateRangeFilter,
168+
WorkforceStageUpdatedRangeFilter]
169+
170+
171+
def _dict_to_graphql_string(d: Union[dict, list, str, int]) -> str:
172+
if isinstance(d, dict):
173+
return "{" + ", ".join(
174+
f'{k}: {_dict_to_graphql_string(v)}' for k, v in d.items()) + "}"
175+
elif isinstance(d, list):
176+
return "[" + ", ".join(
177+
_dict_to_graphql_string(item) for item in d) + "]"
178+
else:
179+
return f'"{d}"' if isinstance(d, str) else str(d)
180+
181+
182+
def build_search_filter(filter: List[SearchFilter]):
183+
"""
184+
Converts a list of search filters to a graphql string
185+
"""
186+
filters = [_dict_to_graphql_string(f.dict()) for f in filter]
187+
return "[" + ", ".join(filters) + "]"

libs/labelbox/tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,3 +1096,21 @@ def embedding(client: Client, environ):
10961096
@pytest.fixture
10971097
def valid_model_id():
10981098
return "2c903542-d1da-48fd-9db1-8c62571bd3d2"
1099+
1100+
1101+
@pytest.fixture
1102+
def requested_labeling_service(
1103+
rand_gen, live_chat_evaluation_project_with_new_dataset,
1104+
chat_evaluation_ontology, model_config):
1105+
project = live_chat_evaluation_project_with_new_dataset
1106+
project.connect_ontology(chat_evaluation_ontology)
1107+
1108+
project.upsert_instructions('tests/integration/media/sample_pdf.pdf')
1109+
1110+
labeling_service = project.get_labeling_service()
1111+
project.add_model_config(model_config.uid)
1112+
project.set_project_model_setup_complete()
1113+
1114+
labeling_service.request()
1115+
1116+
yield project, labeling_service

0 commit comments

Comments
 (0)