From 04c470fa2f476b5f597089ba77e28d7c47bcaee4 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Fri, 16 Aug 2024 15:59:56 -0700 Subject: [PATCH 1/6] Add search filters --- .../src/labelbox/schema/search_filters.py | 122 ++++++++++++++++ .../tests/unit/test_unit_search_filters.py | 137 ++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 libs/labelbox/src/labelbox/schema/search_filters.py create mode 100644 libs/labelbox/tests/unit/test_unit_search_filters.py diff --git a/libs/labelbox/src/labelbox/schema/search_filters.py b/libs/labelbox/src/labelbox/schema/search_filters.py new file mode 100644 index 000000000..cbd47f18c --- /dev/null +++ b/libs/labelbox/src/labelbox/schema/search_filters.py @@ -0,0 +1,122 @@ +import datetime +from enum import Enum +from typing import List, Literal, Union + +from labelbox.pydantic_compat import BaseModel, Field + + +class BaseSearchFilter(BaseModel): + + class Config: + use_enum_values = True + + def dict(self, *args, **kwargs): + res = super().dict(*args, **kwargs) + if 'operation' in res: + res['type'] = res.pop('operation') + + # go through all the keys and convert date to string + for key in res: + if isinstance(res[key], datetime.date): + res[key] = res[key].isoformat() + return res + + +class OperationType(Enum): + Organization = 'organization' + Workspace = 'workspace' + Tag = 'tag' + Stage = 'stage' + WorforceRequestedDate = 'workforce_requested_at' + WorkforceStageUpdatedDate = 'workforce_stage_updated_at' + + +class IdOperator(Enum): + Is = 'is' + + +class DateOperator(Enum): + Equals = 'EQUALS' + GreaterThanOrEqual = 'GREATER_THAN_OR_EQUAL' + LessThanOrEqual = 'LESS_THAN_OR_EQUAL' + + +class DateRangeOperator(Enum): + Between = 'BETWEEN' + + +class OrganizationFilter(BaseSearchFilter): + operation: Literal[OperationType.Organization] + operator: IdOperator + values: List[str] + + +class WorkspaceFilter(BaseSearchFilter): + operation: Literal[OperationType.Workspace] + operator: IdOperator + values: List[str] + + +class TagFilter(BaseSearchFilter): + operation: Literal[OperationType.Tag] + operator: IdOperator + values: List[str] + + +class ProjectStageFilter(BaseSearchFilter): + operation: Literal[OperationType.Stage] + operator: IdOperator + values: List[str] + + +class DateValue(BaseSearchFilter): + operator: DateOperator + value: datetime.date + # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore + + +class WorkforceStageUpdatedFilter(BaseSearchFilter): + operation: Literal[OperationType.WorkforceStageUpdatedDate] + value: DateValue + + +class WorkforceRequestedDateFilter(BaseSearchFilter): + operation: Literal[OperationType.WorforceRequestedDate] + value: DateValue + + +class DateRange(BaseSearchFilter): + min: datetime.date + max: datetime.date + + +class DateRangeValue(BaseSearchFilter): + operator: DateRangeOperator + value: DateRange + + +class WorkforceRequestedDateRangeFilter(BaseSearchFilter): + operation: Literal[OperationType.WorforceRequestedDate] + # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore + value: DateRangeValue + + +class WorkforceStageUpdatedRangeFilter(BaseSearchFilter): + operation: Literal[OperationType.WorkforceStageUpdatedDate] + # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore + value: DateRangeValue + + +SearchFilters = Union[OrganizationFilter, WorkspaceFilter, TagFilter, + ProjectStageFilter, WorkforceRequestedDateFilter, + WorkforceStageUpdatedFilter, + WorkforceRequestedDateRangeFilter, + WorkforceStageUpdatedRangeFilter] + + +def _build_search_filter(filter: List[SearchFilters]): + operation_types = {f.operation for f in filter} + if len(operation_types) < len(filter): + raise ValueError("Only one filter per operation type is allowed") + + return [f.dict() for f in filter] diff --git a/libs/labelbox/tests/unit/test_unit_search_filters.py b/libs/labelbox/tests/unit/test_unit_search_filters.py new file mode 100644 index 000000000..44042ee07 --- /dev/null +++ b/libs/labelbox/tests/unit/test_unit_search_filters.py @@ -0,0 +1,137 @@ +import pytest + +from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, ProjectStageFilter, TagFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkforceStageUpdatedFilter, WorkforceStageUpdatedRangeFilter, WorkspaceFilter, _build_search_filter +from test.test_pdb import pdb + + +def test_duplicate_filters(): + filters = [ + OrganizationFilter(operation=OperationType.Organization, + operator=IdOperator.Is, + values=["clphb4vd7000cd2wv1ktu5cwa"]), + WorkspaceFilter(operation=OperationType.Workspace, + operator=IdOperator.Is, + values=["clphb4vd7000cd2wv1ktu5cwa"]), + TagFilter(operation=OperationType.Tag, + operator=IdOperator.Is, + values=["tag"]), + ProjectStageFilter(operation=OperationType.Stage, + operator=IdOperator.Is, + values=["done"]), + WorkforceRequestedDateFilter( + operation=OperationType.WorforceRequestedDate, + operator=DateOperator.Equals, + value="2024-01-01"), + WorkforceStageUpdatedFilter( + operation=OperationType.WorkforceStageUpdatedDate, + operator=DateOperator.Equals, + value="2024-01-01"), + WorkforceRequestedDateRangeFilter( + operation=OperationType.WorforceRequestedDate, + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min="2024-01-01", + max="2025-01-01"))), + WorkforceStageUpdatedRangeFilter( + operation=OperationType.WorkforceStageUpdatedDate, + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min="2024-01-01", + max="2025-01-01"))) + ] + + with pytest.raises(ValueError): + _build_search_filter(filters) + + +def test_id_filters(): + filters = [ + OrganizationFilter(operation=OperationType.Organization, + operator=IdOperator.Is, + values=["clphb4vd7000cd2wv1ktu5cwa"]), + WorkspaceFilter(operation=OperationType.Workspace, + operator=IdOperator.Is, + values=["clphb4vd7000cd2wv1ktu5cwa"]), + TagFilter(operation=OperationType.Tag, + operator=IdOperator.Is, + values=["tag"]), + ProjectStageFilter(operation=OperationType.Stage, + operator=IdOperator.Is, + values=["requested"]), + ] + + assert _build_search_filter(filters) == [{ + "operator": "is", + "values": ["clphb4vd7000cd2wv1ktu5cwa"], + "type": "organization" + }, { + "operator": "is", + "values": ["clphb4vd7000cd2wv1ktu5cwa"], + "type": "workspace" + }, { + "operator": "is", + "values": ["tag"], + "type": "tag" + }, { + "operator": "is", + "values": ["requested"], + "type": "stage" + }] + + +def test_date_filters(): + filters = [ + WorkforceRequestedDateFilter( + operation=OperationType.WorforceRequestedDate, + value=DateValue(operator=DateOperator.GreaterThanOrEqual, + value="2024-01-01")), + WorkforceStageUpdatedFilter( + operation=OperationType.WorkforceStageUpdatedDate, + value=DateValue(operator=DateOperator.LessThanOrEqual, + value="2025-01-01")), + ] + assert _build_search_filter(filters) == [{ + "type": "workforce_requested_at", + "value": { + "operator": "GREATER_THAN_OR_EQUAL", + "value": "2024-01-01", + } + }, { + "type": "workforce_stage_updated_at", + "value": { + "operator": "LESS_THAN_OR_EQUAL", + "value": "2025-01-01", + } + }] + + +def test_date_range_filters(): + filters = [ + WorkforceRequestedDateRangeFilter( + operation=OperationType.WorforceRequestedDate, + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min="2024-01-01", + max="2025-01-01"))), + WorkforceStageUpdatedRangeFilter( + operation=OperationType.WorkforceStageUpdatedDate, + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min="2024-01-01", + max="2025-01-01"))) + ] + assert _build_search_filter(filters) == [{ + "value": { + "operator": "BETWEEN", + "value": { + "min": "2024-01-01", + "max": "2025-01-01" + } + }, + "type": "workforce_requested_at" + }, { + "value": { + "operator": "BETWEEN", + "value": { + "min": "2024-01-01", + "max": "2025-01-01" + } + }, + "type": "workforce_stage_updated_at" + }] From 8c781f941e6a69d855a6991dcec26d1d034982d6 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Sat, 17 Aug 2024 22:59:06 -0700 Subject: [PATCH 2/6] Integrate search filters with graphql --- docs/labelbox/labeling-service-status.rst | 6 ++ .../schema/labeling_service_dashboard.py | 36 ++++++++---- .../src/labelbox/schema/search_filters.py | 34 ++++++----- libs/labelbox/tests/conftest.py | 18 ++++++ .../integration/test_labeling_dashboard.py | 57 ++++++++++++++++++- .../tests/unit/test_unit_search_filters.py | 49 ++-------------- 6 files changed, 129 insertions(+), 71 deletions(-) create mode 100644 docs/labelbox/labeling-service-status.rst diff --git a/docs/labelbox/labeling-service-status.rst b/docs/labelbox/labeling-service-status.rst new file mode 100644 index 000000000..f2998052d --- /dev/null +++ b/docs/labelbox/labeling-service-status.rst @@ -0,0 +1,6 @@ +Labeling Service Status +=============================================================================================== + +.. automodule:: labelbox.schema.labeling_service_status + :members: + :show-inheritance: diff --git a/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py b/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py index 56f28c865..5514978aa 100644 --- a/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py +++ b/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py @@ -5,6 +5,7 @@ from labelbox.exceptions import ResourceNotFoundError from labelbox.pagination import PaginatedCollection from labelbox.pydantic_compat import BaseModel, root_validator, Field +from labelbox.schema.search_filters import SearchFilter, build_search_filter from labelbox.utils import _CamelCaseMixin from labelbox.schema.labeling_service_status import LabelingServiceStatus @@ -95,22 +96,33 @@ def get_all( cls, client, after: Optional[str] = None, - search_query: Optional[List[Dict]] = None, + search_query: Optional[List[SearchFilter]] = None, ) -> PaginatedCollection: - template = Template( - """query SearchProjectsPyApi($$first: Int, $$from: String) { - searchProjects(input: {after: $$from, searchQuery: $search_query, size: $$first}) - { - nodes { $labeling_dashboard_selections } - pageInfo { endCursor } + + if search_query is not None: + template = Template( + """query SearchProjectsPyApi($$first: Int, $$from: String) { + searchProjects(input: {after: $$from, searchQuery: $search_query, size: $$first}) + { + nodes { $labeling_dashboard_selections } + pageInfo { endCursor } + } + } + """) + else: + template = Template( + """query SearchProjectsPyApi($$first: Int, $$from: String) { + searchProjects(input: {after: $$from, size: $$first}) + { + nodes { $labeling_dashboard_selections } + pageInfo { endCursor } + } } - } - """) - organization_id = client.get_organization().uid + """) query_str = template.substitute( labeling_dashboard_selections=GRAPHQL_QUERY_SELECTIONS, - search_query= - f"[{{type: \"organization\", operator: \"is\", values: [\"{organization_id}\"]}}]" + search_query=build_search_filter(search_query) + if search_query else None, ) params: Dict[str, Union[str, int]] = {} diff --git a/libs/labelbox/src/labelbox/schema/search_filters.py b/libs/labelbox/src/labelbox/schema/search_filters.py index cbd47f18c..20d1fb49d 100644 --- a/libs/labelbox/src/labelbox/schema/search_filters.py +++ b/libs/labelbox/src/labelbox/schema/search_filters.py @@ -107,16 +107,24 @@ class WorkforceStageUpdatedRangeFilter(BaseSearchFilter): value: DateRangeValue -SearchFilters = Union[OrganizationFilter, WorkspaceFilter, TagFilter, - ProjectStageFilter, WorkforceRequestedDateFilter, - WorkforceStageUpdatedFilter, - WorkforceRequestedDateRangeFilter, - WorkforceStageUpdatedRangeFilter] - - -def _build_search_filter(filter: List[SearchFilters]): - operation_types = {f.operation for f in filter} - if len(operation_types) < len(filter): - raise ValueError("Only one filter per operation type is allowed") - - return [f.dict() for f in filter] +SearchFilter = Union[OrganizationFilter, WorkspaceFilter, TagFilter, + ProjectStageFilter, WorkforceRequestedDateFilter, + WorkforceStageUpdatedFilter, + WorkforceRequestedDateRangeFilter, + WorkforceStageUpdatedRangeFilter] + + +def _dict_to_graphql_string(d: Union[dict, list]) -> str: + if isinstance(d, dict): + return "{" + ", ".join( + f'{k}: {_dict_to_graphql_string(v)}' for k, v in d.items()) + "}" + elif isinstance(d, list): + return "[" + ", ".join( + _dict_to_graphql_string(item) for item in d) + "]" + else: + return f'"{d}"' if isinstance(d, str) else str(d) + + +def build_search_filter(filter: List[SearchFilter]): + filters = [_dict_to_graphql_string(f.dict()) for f in filter] + return "[" + ", ".join(filters) + "]" diff --git a/libs/labelbox/tests/conftest.py b/libs/labelbox/tests/conftest.py index eea9851be..0ee53a97c 100644 --- a/libs/labelbox/tests/conftest.py +++ b/libs/labelbox/tests/conftest.py @@ -1096,3 +1096,21 @@ def embedding(client: Client, environ): @pytest.fixture def valid_model_id(): return "2c903542-d1da-48fd-9db1-8c62571bd3d2" + + +@pytest.fixture +def requested_labeling_service( + rand_gen, live_chat_evaluation_project_with_new_dataset, + chat_evaluation_ontology, model_config): + project = live_chat_evaluation_project_with_new_dataset + project.connect_ontology(chat_evaluation_ontology) + + project.upsert_instructions('tests/integration/media/sample_pdf.pdf') + + labeling_service = project.get_labeling_service() + project.add_model_config(model_config.uid) + project.set_project_model_setup_complete() + + labeling_service.request() + + yield project, labeling_service diff --git a/libs/labelbox/tests/integration/test_labeling_dashboard.py b/libs/labelbox/tests/integration/test_labeling_dashboard.py index a45334bcc..384023fbc 100644 --- a/libs/labelbox/tests/integration/test_labeling_dashboard.py +++ b/libs/labelbox/tests/integration/test_labeling_dashboard.py @@ -1,7 +1,9 @@ +from datetime import datetime, timedelta from labelbox.schema.labeling_service import LabelingServiceStatus +from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkspaceFilter -def test_request_labeling_service_moe_offline_project( +def test_request_labeling_service_dashboard( rand_gen, offline_chat_evaluation_project, chat_evaluation_ontology, offline_conversational_data_row): project = offline_chat_evaluation_project @@ -22,3 +24,56 @@ def test_request_labeling_service_moe_offline_project( assert labeling_service_dashboard.status == LabelingServiceStatus.Missing assert labeling_service_dashboard.tasks_completed == 0 assert labeling_service_dashboard.tasks_remaining == 0 + + +def test_request_labeling_service_dashboard_filters(requested_labeling_service): + project, _ = requested_labeling_service + + organization = project.client.get_organization() + org_filter = OrganizationFilter( + operation=OperationType.Organization, + operator=IdOperator.Is, + values=[organization.uid]) + + labeling_service_dashboard = [ + ld for ld in project.client.get_labeling_service_dashboards(search_query=[org_filter]) + ][0] + assert labeling_service_dashboard is not None + + workforce_requested_filter_before = WorkforceRequestedDateFilter( + operation=OperationType.WorforceRequestedDate, + value=DateValue(operator=DateOperator.GreaterThanOrEqual, + value="2024-01-01")) + year_from_now = (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d") + workforce_requested_filter_after = WorkforceRequestedDateFilter( + operation=OperationType.WorforceRequestedDate, + value=DateValue(operator=DateOperator.LessThanOrEqual, + value=year_from_now)) + + labeling_service_dashboard = [ + ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_requested_filter_after, workforce_requested_filter_before]) + ][0] + assert labeling_service_dashboard is not None + + workforce_date_range_filter = WorkforceRequestedDateRangeFilter( + operation=OperationType.WorforceRequestedDate, + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min="2024-01-01", + max=year_from_now))) + + labeling_service_dashboard = [ + ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_date_range_filter]) + ][0] + assert labeling_service_dashboard is not None + + # with non existing data + workspace_id = "clzzu4rme000008l42vnl4kre" + workspace_filter = WorkspaceFilter( + operation=OperationType.Workspace, + operator=IdOperator.Is, + values=[workspace_id]) + labeling_service_dashboard = [ + ld for ld in project.client.get_labeling_service_dashboards(search_query=[workspace_filter]) + ] + assert len(labeling_service_dashboard) == 0 + assert labeling_service_dashboard == [] diff --git a/libs/labelbox/tests/unit/test_unit_search_filters.py b/libs/labelbox/tests/unit/test_unit_search_filters.py index 44042ee07..0c88c42cd 100644 --- a/libs/labelbox/tests/unit/test_unit_search_filters.py +++ b/libs/labelbox/tests/unit/test_unit_search_filters.py @@ -1,45 +1,4 @@ -import pytest - -from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, ProjectStageFilter, TagFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkforceStageUpdatedFilter, WorkforceStageUpdatedRangeFilter, WorkspaceFilter, _build_search_filter -from test.test_pdb import pdb - - -def test_duplicate_filters(): - filters = [ - OrganizationFilter(operation=OperationType.Organization, - operator=IdOperator.Is, - values=["clphb4vd7000cd2wv1ktu5cwa"]), - WorkspaceFilter(operation=OperationType.Workspace, - operator=IdOperator.Is, - values=["clphb4vd7000cd2wv1ktu5cwa"]), - TagFilter(operation=OperationType.Tag, - operator=IdOperator.Is, - values=["tag"]), - ProjectStageFilter(operation=OperationType.Stage, - operator=IdOperator.Is, - values=["done"]), - WorkforceRequestedDateFilter( - operation=OperationType.WorforceRequestedDate, - operator=DateOperator.Equals, - value="2024-01-01"), - WorkforceStageUpdatedFilter( - operation=OperationType.WorkforceStageUpdatedDate, - operator=DateOperator.Equals, - value="2024-01-01"), - WorkforceRequestedDateRangeFilter( - operation=OperationType.WorforceRequestedDate, - value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01", - max="2025-01-01"))), - WorkforceStageUpdatedRangeFilter( - operation=OperationType.WorkforceStageUpdatedDate, - value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01", - max="2025-01-01"))) - ] - - with pytest.raises(ValueError): - _build_search_filter(filters) +from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, ProjectStageFilter, TagFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkforceStageUpdatedFilter, WorkforceStageUpdatedRangeFilter, WorkspaceFilter, build_search_filter def test_id_filters(): @@ -58,7 +17,7 @@ def test_id_filters(): values=["requested"]), ] - assert _build_search_filter(filters) == [{ + assert build_search_filter(filters) == [{ "operator": "is", "values": ["clphb4vd7000cd2wv1ktu5cwa"], "type": "organization" @@ -88,7 +47,7 @@ def test_date_filters(): value=DateValue(operator=DateOperator.LessThanOrEqual, value="2025-01-01")), ] - assert _build_search_filter(filters) == [{ + assert build_search_filter(filters) == [{ "type": "workforce_requested_at", "value": { "operator": "GREATER_THAN_OR_EQUAL", @@ -116,7 +75,7 @@ def test_date_range_filters(): value=DateRange(min="2024-01-01", max="2025-01-01"))) ] - assert _build_search_filter(filters) == [{ + assert build_search_filter(filters) == [{ "value": { "operator": "BETWEEN", "value": { From 7fa8f5dfabbeb829e59b1ce312bbed2e9e1ac2a6 Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Sun, 18 Aug 2024 10:52:31 -0700 Subject: [PATCH 3/6] Use datetime instead of zone --- .../src/labelbox/schema/search_filters.py | 16 ++-- .../tests/unit/test_unit_search_filters.py | 75 +++++-------------- 2 files changed, 26 insertions(+), 65 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/search_filters.py b/libs/labelbox/src/labelbox/schema/search_filters.py index 20d1fb49d..a03ce6bf8 100644 --- a/libs/labelbox/src/labelbox/schema/search_filters.py +++ b/libs/labelbox/src/labelbox/schema/search_filters.py @@ -2,7 +2,8 @@ from enum import Enum from typing import List, Literal, Union -from labelbox.pydantic_compat import BaseModel, Field +from labelbox.pydantic_compat import BaseModel +from labelbox.utils import format_iso_datetime class BaseSearchFilter(BaseModel): @@ -17,8 +18,8 @@ def dict(self, *args, **kwargs): # go through all the keys and convert date to string for key in res: - if isinstance(res[key], datetime.date): - res[key] = res[key].isoformat() + if isinstance(res[key], datetime.datetime): + res[key] = format_iso_datetime(res[key]) return res @@ -71,8 +72,7 @@ class ProjectStageFilter(BaseSearchFilter): class DateValue(BaseSearchFilter): operator: DateOperator - value: datetime.date - # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore + value: datetime.datetime class WorkforceStageUpdatedFilter(BaseSearchFilter): @@ -86,8 +86,8 @@ class WorkforceRequestedDateFilter(BaseSearchFilter): class DateRange(BaseSearchFilter): - min: datetime.date - max: datetime.date + min: datetime.datetime + max: datetime.datetime class DateRangeValue(BaseSearchFilter): @@ -97,13 +97,11 @@ class DateRangeValue(BaseSearchFilter): class WorkforceRequestedDateRangeFilter(BaseSearchFilter): operation: Literal[OperationType.WorforceRequestedDate] - # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore value: DateRangeValue class WorkforceStageUpdatedRangeFilter(BaseSearchFilter): operation: Literal[OperationType.WorkforceStageUpdatedDate] - # timezone: TimeZoneName = Field(default=TimeZoneName.UTC) # type: ignore value: DateRangeValue diff --git a/libs/labelbox/tests/unit/test_unit_search_filters.py b/libs/labelbox/tests/unit/test_unit_search_filters.py index 0c88c42cd..5beadd142 100644 --- a/libs/labelbox/tests/unit/test_unit_search_filters.py +++ b/libs/labelbox/tests/unit/test_unit_search_filters.py @@ -1,3 +1,4 @@ +from datetime import date, datetime from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, ProjectStageFilter, TagFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkforceStageUpdatedFilter, WorkforceStageUpdatedRangeFilter, WorkspaceFilter, build_search_filter @@ -17,23 +18,9 @@ def test_id_filters(): values=["requested"]), ] - assert build_search_filter(filters) == [{ - "operator": "is", - "values": ["clphb4vd7000cd2wv1ktu5cwa"], - "type": "organization" - }, { - "operator": "is", - "values": ["clphb4vd7000cd2wv1ktu5cwa"], - "type": "workspace" - }, { - "operator": "is", - "values": ["tag"], - "type": "tag" - }, { - "operator": "is", - "values": ["requested"], - "type": "stage" - }] + assert build_search_filter( + filters + ) == '[{operator: "is", values: ["clphb4vd7000cd2wv1ktu5cwa"], type: "organization"}, {operator: "is", values: ["clphb4vd7000cd2wv1ktu5cwa"], type: "workspace"}, {operator: "is", values: ["tag"], type: "tag"}, {operator: "is", values: ["requested"], type: "stage"}]' def test_date_filters(): @@ -41,56 +28,32 @@ def test_date_filters(): WorkforceRequestedDateFilter( operation=OperationType.WorforceRequestedDate, value=DateValue(operator=DateOperator.GreaterThanOrEqual, - value="2024-01-01")), + value=datetime.strptime("2024-01-01", "%Y-%m-%d"))), WorkforceStageUpdatedFilter( operation=OperationType.WorkforceStageUpdatedDate, value=DateValue(operator=DateOperator.LessThanOrEqual, - value="2025-01-01")), + value=datetime.strptime("2025-01-01", "%Y-%m-%d"))), ] - assert build_search_filter(filters) == [{ - "type": "workforce_requested_at", - "value": { - "operator": "GREATER_THAN_OR_EQUAL", - "value": "2024-01-01", - } - }, { - "type": "workforce_stage_updated_at", - "value": { - "operator": "LESS_THAN_OR_EQUAL", - "value": "2025-01-01", - } - }] + assert build_search_filter( + filters + ) == '[{value: {operator: "GREATER_THAN_OR_EQUAL", value: "2024-01-01T08:00:00Z"}, type: "workforce_requested_at"}, {value: {operator: "LESS_THAN_OR_EQUAL", value: "2025-01-01T08:00:00Z"}, type: "workforce_stage_updated_at"}]' def test_date_range_filters(): filters = [ WorkforceRequestedDateRangeFilter( operation=OperationType.WorforceRequestedDate, - value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01", - max="2025-01-01"))), + value=DateRangeValue( + operator=DateRangeOperator.Between, + value=DateRange(min=datetime.strptime("2024-01-01", "%Y-%m-%d"), + max=datetime.strptime("2025-01-01", + "%Y-%m-%d")))), WorkforceStageUpdatedRangeFilter( operation=OperationType.WorkforceStageUpdatedDate, value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01", - max="2025-01-01"))) + value=DateRange(min="2024-01-01T08:00:00Z", + max="2025-01-01T08:00:00Z"))) ] - assert build_search_filter(filters) == [{ - "value": { - "operator": "BETWEEN", - "value": { - "min": "2024-01-01", - "max": "2025-01-01" - } - }, - "type": "workforce_requested_at" - }, { - "value": { - "operator": "BETWEEN", - "value": { - "min": "2024-01-01", - "max": "2025-01-01" - } - }, - "type": "workforce_stage_updated_at" - }] + assert build_search_filter( + filters + ) == '[{value: {operator: "BETWEEN", value: {min: "2024-01-01T08:00:00Z", max: "2025-01-01T08:00:00Z"}}, type: "workforce_requested_at"}, {value: {operator: "BETWEEN", value: {min: "2024-01-01T08:00:00Z", max: "2025-01-01T08:00:00Z"}}, type: "workforce_stage_updated_at"}]' From 850810133ad420d2262c76bd9a05da8610ca37aa Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Sun, 18 Aug 2024 15:54:36 -0700 Subject: [PATCH 4/6] Add readdoc files and update docstring(s) --- docs/labelbox/index.rst | 2 + docs/labelbox/labeling-service-dashboard.rst | 6 ++ docs/labelbox/search-filters.rst | 7 +++ libs/labelbox/src/labelbox/client.py | 31 ++++++++-- .../src/labelbox/schema/search_filters.py | 56 +++++++++++++++++++ 5 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 docs/labelbox/labeling-service-dashboard.rst create mode 100644 docs/labelbox/search-filters.rst diff --git a/docs/labelbox/index.rst b/docs/labelbox/index.rst index 35bcd03a1..35118f56f 100644 --- a/docs/labelbox/index.rst +++ b/docs/labelbox/index.rst @@ -30,6 +30,7 @@ Labelbox Python SDK Documentation labeling-frontend-options labeling-parameter-override labeling-service + labeling-service-dashboard model model-config model-run @@ -42,6 +43,7 @@ Labelbox Python SDK Documentation quality-mode resource-tag review + search-filters send-to-annotate-params slice task diff --git a/docs/labelbox/labeling-service-dashboard.rst b/docs/labelbox/labeling-service-dashboard.rst new file mode 100644 index 000000000..b0f0ec222 --- /dev/null +++ b/docs/labelbox/labeling-service-dashboard.rst @@ -0,0 +1,6 @@ +Labeling Service +=============================================================================================== + +.. automodule:: labelbox.schema.search_filters + :members: + :show-inheritance: diff --git a/docs/labelbox/search-filters.rst b/docs/labelbox/search-filters.rst new file mode 100644 index 000000000..413cdd831 --- /dev/null +++ b/docs/labelbox/search-filters.rst @@ -0,0 +1,7 @@ +Labeling Service +=============================================================================================== + +.. automodule:: labelbox.schema.labeling_service_dashboard + :members: + :exclude-members: _dict_to_graphql_string + :show-inheritance: diff --git a/libs/labelbox/src/labelbox/client.py b/libs/labelbox/src/labelbox/client.py index dd70baaef..71a90056f 100644 --- a/libs/labelbox/src/labelbox/client.py +++ b/libs/labelbox/src/labelbox/client.py @@ -11,6 +11,7 @@ from datetime import datetime, timezone from typing import Any, List, Dict, Union, Optional, overload, Callable +from labelbox.schema.search_filters import SearchFilter import requests import requests.exceptions from google.api_core import retry @@ -2410,16 +2411,38 @@ def upsert_label_feedback(self, label_id: str, feedback: str, def get_labeling_service_dashboards( self, after: Optional[str] = None, - search_query: Optional[List[Dict]] = None, + search_query: Optional[List[SearchFilter]] = None, ) -> PaginatedCollection: """ Get all labeling service dashboards for a given org. Optional parameters: - after: The cursor to use for pagination. - where: A filter to apply to the query. + search_query: A list of search filters representing the search + + after: The cursor to use for pagination. - NOTE: support for after and search_query are not yet implemented. + NOTE: + - Retrieves all projects for the organization or as filtered by the search query. + - Sorted by project created date in ascending order. + + Examples: + Retrieves all labeling service dashboards for a given workspace id: + >>> workspace_filter = WorkspaceFilter( + >>> operation=OperationType.Workspace, + >>> operator=IdOperator.Is, + >>> values=[workspace_id]) + >>> labeling_service_dashboard = [ + >>> ld for ld in project.client.get_labeling_service_dashboards(search_query=[workspace_filter])] + + Retrieves all labeling service dashboards requested less than 7 days ago: + >>> seven_days_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + >>> workforce_requested_filter_before = WorkforceRequestedDateFilter( + >>> operation=OperationType.WorforceRequestedDate, + >>> value=DateValue(operator=DateOperator.GreaterThanOrEqual, + >>> value=seven_days_ago)) + >>> labeling_service_dashboard = [ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_requested_filter_before])] + + See libs/labelbox/src/labelbox/schema/search_filters.py and libs/labelbox/tests/unit/test_unit_search_filters.py for more examples. """ return LabelingServiceDashboard.get_all(self, after, diff --git a/libs/labelbox/src/labelbox/schema/search_filters.py b/libs/labelbox/src/labelbox/schema/search_filters.py index a03ce6bf8..2c99f7b52 100644 --- a/libs/labelbox/src/labelbox/schema/search_filters.py +++ b/libs/labelbox/src/labelbox/schema/search_filters.py @@ -7,6 +7,9 @@ class BaseSearchFilter(BaseModel): + """ + Shared code for all search filters + """ class Config: use_enum_values = True @@ -24,6 +27,9 @@ def dict(self, *args, **kwargs): class OperationType(Enum): + """ + Supported search entity types + """ Organization = 'organization' Workspace = 'workspace' Tag = 'tag' @@ -33,74 +39,121 @@ class OperationType(Enum): class IdOperator(Enum): + """ + Supported operators for ids + """ Is = 'is' class DateOperator(Enum): + """ + Supported operators for dates + """ Equals = 'EQUALS' GreaterThanOrEqual = 'GREATER_THAN_OR_EQUAL' LessThanOrEqual = 'LESS_THAN_OR_EQUAL' class DateRangeOperator(Enum): + """ + Supported operators for date ranges + """ Between = 'BETWEEN' class OrganizationFilter(BaseSearchFilter): + """ + Filter for organization + """ operation: Literal[OperationType.Organization] operator: IdOperator values: List[str] class WorkspaceFilter(BaseSearchFilter): + """ + Filter for workspace + """ operation: Literal[OperationType.Workspace] operator: IdOperator values: List[str] class TagFilter(BaseSearchFilter): + """ + Filter for project tags + """ operation: Literal[OperationType.Tag] operator: IdOperator values: List[str] class ProjectStageFilter(BaseSearchFilter): + """ + Filter labelbox service / aka project stages + """ operation: Literal[OperationType.Stage] operator: IdOperator values: List[str] class DateValue(BaseSearchFilter): + """ + Date value for a search filter + + Date formats: + datetime: an existing datetime object + str the following formats are accepted: YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM] + default timezone is UTC + """ operator: DateOperator value: datetime.datetime class WorkforceStageUpdatedFilter(BaseSearchFilter): + """ + Filter for workforce stage updated date + """ operation: Literal[OperationType.WorkforceStageUpdatedDate] value: DateValue class WorkforceRequestedDateFilter(BaseSearchFilter): + """ + Filter for workforce requested date + """ operation: Literal[OperationType.WorforceRequestedDate] value: DateValue class DateRange(BaseSearchFilter): + """ + Date range for a search filter + """ min: datetime.datetime max: datetime.datetime class DateRangeValue(BaseSearchFilter): + """ + Date range value for a search filter + """ operator: DateRangeOperator value: DateRange class WorkforceRequestedDateRangeFilter(BaseSearchFilter): + """ + Filter for workforce requested date range + """ operation: Literal[OperationType.WorforceRequestedDate] value: DateRangeValue class WorkforceStageUpdatedRangeFilter(BaseSearchFilter): + """ + Filter for workforce stage updated date range + """ operation: Literal[OperationType.WorkforceStageUpdatedDate] value: DateRangeValue @@ -124,5 +177,8 @@ def _dict_to_graphql_string(d: Union[dict, list]) -> str: def build_search_filter(filter: List[SearchFilter]): + """ + Converts a list of search filters to a graphql string + """ filters = [_dict_to_graphql_string(f.dict()) for f in filter] return "[" + ", ".join(filters) + "]" From 2fd7a2c86b2b85591e29c0bc3c7601f6729ac18b Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Sun, 18 Aug 2024 15:57:25 -0700 Subject: [PATCH 5/6] Removed unused after param --- docs/labelbox/labeling-service-dashboard.rst | 5 ++--- docs/labelbox/search-filters.rst | 4 ++-- libs/labelbox/src/labelbox/client.py | 7 +------ .../src/labelbox/schema/labeling_service_dashboard.py | 3 --- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/docs/labelbox/labeling-service-dashboard.rst b/docs/labelbox/labeling-service-dashboard.rst index b0f0ec222..5cf5c8cb7 100644 --- a/docs/labelbox/labeling-service-dashboard.rst +++ b/docs/labelbox/labeling-service-dashboard.rst @@ -1,6 +1,5 @@ -Labeling Service +Labeling Service Dashboard =============================================================================================== -.. automodule:: labelbox.schema.search_filters - :members: +.. automodule:: labelbox.schema.labeling_service_dashboard :show-inheritance: diff --git a/docs/labelbox/search-filters.rst b/docs/labelbox/search-filters.rst index 413cdd831..01ff0b2c8 100644 --- a/docs/labelbox/search-filters.rst +++ b/docs/labelbox/search-filters.rst @@ -1,7 +1,7 @@ -Labeling Service +Search Filters =============================================================================================== -.. automodule:: labelbox.schema.labeling_service_dashboard +.. automodule:: labelbox.schema.search_filters :members: :exclude-members: _dict_to_graphql_string :show-inheritance: diff --git a/libs/labelbox/src/labelbox/client.py b/libs/labelbox/src/labelbox/client.py index 71a90056f..b92ae9b05 100644 --- a/libs/labelbox/src/labelbox/client.py +++ b/libs/labelbox/src/labelbox/client.py @@ -2410,7 +2410,6 @@ def upsert_label_feedback(self, label_id: str, feedback: str, def get_labeling_service_dashboards( self, - after: Optional[str] = None, search_query: Optional[List[SearchFilter]] = None, ) -> PaginatedCollection: """ @@ -2419,8 +2418,6 @@ def get_labeling_service_dashboards( Optional parameters: search_query: A list of search filters representing the search - after: The cursor to use for pagination. - NOTE: - Retrieves all projects for the organization or as filtered by the search query. - Sorted by project created date in ascending order. @@ -2444,9 +2441,7 @@ def get_labeling_service_dashboards( See libs/labelbox/src/labelbox/schema/search_filters.py and libs/labelbox/tests/unit/test_unit_search_filters.py for more examples. """ - return LabelingServiceDashboard.get_all(self, - after, - search_query=search_query) + return LabelingServiceDashboard.get_all(self, search_query=search_query) def get_task_by_id(self, task_id: str) -> Union[Task, DataUpsertTask]: """ diff --git a/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py b/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py index 5514978aa..9621008e3 100644 --- a/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py +++ b/libs/labelbox/src/labelbox/schema/labeling_service_dashboard.py @@ -95,7 +95,6 @@ def get(cls, client, project_id: str) -> 'LabelingServiceDashboard': def get_all( cls, client, - after: Optional[str] = None, search_query: Optional[List[SearchFilter]] = None, ) -> PaginatedCollection: @@ -126,8 +125,6 @@ def get_all( ) params: Dict[str, Union[str, int]] = {} - if after: - params = {"from": after} def convert_to_labeling_service_dashboard(client, data): data['client'] = client From 41d27eb042d378d703b72bf859a4e1ba8a78aabc Mon Sep 17 00:00:00 2001 From: Val Brodsky Date: Mon, 19 Aug 2024 12:06:43 -0700 Subject: [PATCH 6/6] Fix tests --- .../src/labelbox/schema/search_filters.py | 7 ++- .../integration/test_labeling_dashboard.py | 43 +++++++++++-------- libs/labelbox/tests/integration/test_task.py | 2 - .../tests/unit/test_unit_search_filters.py | 38 ++++++++++------ 4 files changed, 54 insertions(+), 36 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/search_filters.py b/libs/labelbox/src/labelbox/schema/search_filters.py index 2c99f7b52..976071086 100644 --- a/libs/labelbox/src/labelbox/schema/search_filters.py +++ b/libs/labelbox/src/labelbox/schema/search_filters.py @@ -104,7 +104,10 @@ class DateValue(BaseSearchFilter): Date formats: datetime: an existing datetime object str the following formats are accepted: YYYY-MM-DD[T]HH:MM[:SS[.ffffff]][Z or [±]HH[:]MM] - default timezone is UTC + NOTE + if a date / datetime string is passed without a timezone, we will assume the time is UTC and convert it to a local timezone + 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' + while the same string in EST will get converted to '2024-01-01T05:00:00Z' """ operator: DateOperator value: datetime.datetime @@ -165,7 +168,7 @@ class WorkforceStageUpdatedRangeFilter(BaseSearchFilter): WorkforceStageUpdatedRangeFilter] -def _dict_to_graphql_string(d: Union[dict, list]) -> str: +def _dict_to_graphql_string(d: Union[dict, list, str, int]) -> str: if isinstance(d, dict): return "{" + ", ".join( f'{k}: {_dict_to_graphql_string(v)}' for k, v in d.items()) + "}" diff --git a/libs/labelbox/tests/integration/test_labeling_dashboard.py b/libs/labelbox/tests/integration/test_labeling_dashboard.py index 384023fbc..8289be90d 100644 --- a/libs/labelbox/tests/integration/test_labeling_dashboard.py +++ b/libs/labelbox/tests/integration/test_labeling_dashboard.py @@ -3,9 +3,10 @@ from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkspaceFilter -def test_request_labeling_service_dashboard( - rand_gen, offline_chat_evaluation_project, chat_evaluation_ontology, - offline_conversational_data_row): +def test_request_labeling_service_dashboard(rand_gen, + offline_chat_evaluation_project, + chat_evaluation_ontology, + offline_conversational_data_row): project = offline_chat_evaluation_project project.connect_ontology(chat_evaluation_ontology) @@ -30,50 +31,54 @@ def test_request_labeling_service_dashboard_filters(requested_labeling_service): project, _ = requested_labeling_service organization = project.client.get_organization() - org_filter = OrganizationFilter( - operation=OperationType.Organization, - operator=IdOperator.Is, - values=[organization.uid]) + org_filter = OrganizationFilter(operation=OperationType.Organization, + operator=IdOperator.Is, + values=[organization.uid]) labeling_service_dashboard = [ - ld for ld in project.client.get_labeling_service_dashboards(search_query=[org_filter]) + ld for ld in project.client.get_labeling_service_dashboards( + search_query=[org_filter]) ][0] assert labeling_service_dashboard is not None workforce_requested_filter_before = WorkforceRequestedDateFilter( operation=OperationType.WorforceRequestedDate, value=DateValue(operator=DateOperator.GreaterThanOrEqual, - value="2024-01-01")) - year_from_now = (datetime.now() + timedelta(days=365)).strftime("%Y-%m-%d") + value=datetime.strptime("2024-01-01", "%Y-%m-%d"))) + year_from_now = (datetime.now() + timedelta(days=365)) workforce_requested_filter_after = WorkforceRequestedDateFilter( operation=OperationType.WorforceRequestedDate, value=DateValue(operator=DateOperator.LessThanOrEqual, value=year_from_now)) labeling_service_dashboard = [ - ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_requested_filter_after, workforce_requested_filter_before]) + ld + for ld in project.client.get_labeling_service_dashboards(search_query=[ + workforce_requested_filter_after, workforce_requested_filter_before + ]) ][0] assert labeling_service_dashboard is not None workforce_date_range_filter = WorkforceRequestedDateRangeFilter( operation=OperationType.WorforceRequestedDate, value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01", - max=year_from_now))) + value=DateRange(min="2024-01-01T00:00:00-0800", + max=year_from_now))) labeling_service_dashboard = [ - ld for ld in project.client.get_labeling_service_dashboards(search_query=[workforce_date_range_filter]) + ld for ld in project.client.get_labeling_service_dashboards( + search_query=[workforce_date_range_filter]) ][0] assert labeling_service_dashboard is not None # with non existing data workspace_id = "clzzu4rme000008l42vnl4kre" - workspace_filter = WorkspaceFilter( - operation=OperationType.Workspace, - operator=IdOperator.Is, - values=[workspace_id]) + workspace_filter = WorkspaceFilter(operation=OperationType.Workspace, + operator=IdOperator.Is, + values=[workspace_id]) labeling_service_dashboard = [ - ld for ld in project.client.get_labeling_service_dashboards(search_query=[workspace_filter]) + ld for ld in project.client.get_labeling_service_dashboards( + search_query=[workspace_filter]) ] assert len(labeling_service_dashboard) == 0 assert labeling_service_dashboard == [] diff --git a/libs/labelbox/tests/integration/test_task.py b/libs/labelbox/tests/integration/test_task.py index 8ab4bd4e3..b0eac2fa1 100644 --- a/libs/labelbox/tests/integration/test_task.py +++ b/libs/labelbox/tests/integration/test_task.py @@ -1,6 +1,4 @@ import json -from labelbox.schema.disconnected_task import DisconnectedTask -import pytest import collections.abc from labelbox import DataRow from labelbox.schema.data_row_metadata import DataRowMetadataField diff --git a/libs/labelbox/tests/unit/test_unit_search_filters.py b/libs/labelbox/tests/unit/test_unit_search_filters.py index 5beadd142..5ea679a24 100644 --- a/libs/labelbox/tests/unit/test_unit_search_filters.py +++ b/libs/labelbox/tests/unit/test_unit_search_filters.py @@ -1,5 +1,6 @@ -from datetime import date, datetime +from datetime import datetime from labelbox.schema.search_filters import DateOperator, DateRange, DateRangeOperator, DateRangeValue, DateValue, IdOperator, OperationType, OrganizationFilter, ProjectStageFilter, TagFilter, WorkforceRequestedDateFilter, WorkforceRequestedDateRangeFilter, WorkforceStageUpdatedFilter, WorkforceStageUpdatedRangeFilter, WorkspaceFilter, build_search_filter +from labelbox.utils import format_iso_datetime def test_id_filters(): @@ -24,35 +25,46 @@ def test_id_filters(): def test_date_filters(): + local_time_start = datetime.strptime("2024-01-01", "%Y-%m-%d") + local_time_end = datetime.strptime("2025-01-01", "%Y-%m-%d") + filters = [ WorkforceRequestedDateFilter( operation=OperationType.WorforceRequestedDate, value=DateValue(operator=DateOperator.GreaterThanOrEqual, - value=datetime.strptime("2024-01-01", "%Y-%m-%d"))), + value=local_time_start)), WorkforceStageUpdatedFilter( operation=OperationType.WorkforceStageUpdatedDate, value=DateValue(operator=DateOperator.LessThanOrEqual, - value=datetime.strptime("2025-01-01", "%Y-%m-%d"))), + value=local_time_end)), ] - assert build_search_filter( - filters - ) == '[{value: {operator: "GREATER_THAN_OR_EQUAL", value: "2024-01-01T08:00:00Z"}, type: "workforce_requested_at"}, {value: {operator: "LESS_THAN_OR_EQUAL", value: "2025-01-01T08:00:00Z"}, type: "workforce_stage_updated_at"}]' + expected_start = format_iso_datetime(local_time_start) + expected_end = format_iso_datetime(local_time_end) + + expected = '[{value: {operator: "GREATER_THAN_OR_EQUAL", value: "' + expected_start + '"}, type: "workforce_requested_at"}, {value: {operator: "LESS_THAN_OR_EQUAL", value: "' + expected_end + '"}, type: "workforce_stage_updated_at"}]' + assert build_search_filter(filters) == expected def test_date_range_filters(): filters = [ WorkforceRequestedDateRangeFilter( operation=OperationType.WorforceRequestedDate, - value=DateRangeValue( - operator=DateRangeOperator.Between, - value=DateRange(min=datetime.strptime("2024-01-01", "%Y-%m-%d"), - max=datetime.strptime("2025-01-01", - "%Y-%m-%d")))), + value=DateRangeValue(operator=DateRangeOperator.Between, + value=DateRange(min=datetime.strptime( + "2024-01-01T00:00:00-0800", + "%Y-%m-%dT%H:%M:%S%z"), + max=datetime.strptime( + "2025-01-01T00:00:00-0800", + "%Y-%m-%dT%H:%M:%S%z")))), WorkforceStageUpdatedRangeFilter( operation=OperationType.WorkforceStageUpdatedDate, value=DateRangeValue(operator=DateRangeOperator.Between, - value=DateRange(min="2024-01-01T08:00:00Z", - max="2025-01-01T08:00:00Z"))) + value=DateRange(min=datetime.strptime( + "2024-01-01T00:00:00-0800", + "%Y-%m-%dT%H:%M:%S%z"), + max=datetime.strptime( + "2025-01-01T00:00:00-0800", + "%Y-%m-%dT%H:%M:%S%z")))), ] assert build_search_filter( filters