Skip to content

Commit 8e5c65d

Browse files
committed
Add last_activity_at / label_created_at export v2 support
1 parent 5af2944 commit 8e5c65d

File tree

3 files changed

+153
-15
lines changed

3 files changed

+153
-15
lines changed

labelbox/schema/export_filters.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import sys
2+
3+
from typing import Optional
4+
if sys.version_info >= (3, 8):
5+
from typing import TypedDict, Tuple
6+
else:
7+
from typing_extensions import TypedDict
8+
9+
10+
class ProjectExportFilters(TypedDict):
11+
label_created_at: Optional[Tuple[str, str]]
12+
last_activity_at: Optional[Tuple[str, str]]

labelbox/schema/project.py

Lines changed: 124 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from labelbox.pagination import PaginatedCollection
2121
from labelbox.schema.consensus_settings import ConsensusSettings
2222
from labelbox.schema.data_row import DataRow
23+
from labelbox.schema.export_filters import ProjectExportFilters
2324
from labelbox.schema.export_params import ProjectExportParams
2425
from labelbox.schema.media_type import MediaType
2526
from labelbox.schema.queue_mode import QueueMode
@@ -46,6 +47,20 @@
4647
logger = logging.getLogger(__name__)
4748

4849

50+
def _validate_datetime(string_date: str) -> bool:
51+
"""helper function validate that datetime is as follows: YYYY-MM-DD for the export"""
52+
if string_date:
53+
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
54+
try:
55+
datetime.strptime(string_date, fmt)
56+
return True
57+
except ValueError:
58+
pass
59+
raise ValueError(f"""Incorrect format for: {string_date}.
60+
Format must be \"YYYY-MM-DD\" or \"YYYY-MM-DD hh:mm:ss\"""")
61+
return True
62+
63+
4964
class Project(DbObject, Updateable, Deletable):
5065
""" A Project is a container that includes a labeling frontend, an ontology,
5166
datasets and labels.
@@ -337,19 +352,6 @@ def _string_from_dict(dictionary: dict, value_with_quotes=False) -> str:
337352
if dictionary.get(c)
338353
])
339354

340-
def _validate_datetime(string_date: str) -> bool:
341-
"""helper function validate that datetime is as follows: YYYY-MM-DD for the export"""
342-
if string_date:
343-
for fmt in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S"):
344-
try:
345-
datetime.strptime(string_date, fmt)
346-
return True
347-
except ValueError:
348-
pass
349-
raise ValueError(f"""Incorrect format for: {string_date}.
350-
Format must be \"YYYY-MM-DD\" or \"YYYY-MM-DD hh:mm:ss\"""")
351-
return True
352-
353355
sleep_time = 2
354356
id_param = "projectId"
355357
filter_param = ""
@@ -403,12 +405,24 @@ def _validate_datetime(string_date: str) -> bool:
403405
"""
404406
Creates a project run export task with the given params and returns the task.
405407
406-
>>> export_task = export_v2("my_export_task", filter={"media_attributes": True})
408+
>>> task = project.export_v2(
409+
>>> filters={
410+
>>> "last_activity_at": ["2000-01-01 00:00:00", "2050-01-01 00:00:00"],
411+
>>> "label_created_at": ["2000-01-01 00:00:00", "2050-01-01 00:00:00"]
412+
>>> },
413+
>>> params={
414+
>>> "include_performance_details": False,
415+
>>> "include_labels": True
416+
>>> })
417+
>>> task.wait_till_done()
418+
>>> task.result
419+
407420
408421
"""
409422

410423
def export_v2(self,
411424
task_name: Optional[str] = None,
425+
filters: ProjectExportFilters = None,
412426
params: Optional[ProjectExportParams] = None) -> Task:
413427

414428
_params = params or ProjectExportParams({
@@ -420,6 +434,15 @@ def export_v2(self,
420434
"label_details": False
421435
})
422436

437+
_filters = filters or ProjectExportFilters()
438+
439+
def _get_timezone() -> str:
440+
timezone_query_str = """query CurrentUserPyApi { user { timezone } }"""
441+
tz_res = self.client.execute(timezone_query_str)
442+
return tz_res["user"]["timezone"] or "UTC"
443+
444+
timezone: Optional[str] = None
445+
423446
mutation_name = "exportDataRowsInProject"
424447
create_task_query_str = """mutation exportDataRowsInProjectPyApi($input: ExportDataRowsInProjectInput!){
425448
%s(input: $input) {taskId} }
@@ -428,7 +451,11 @@ def export_v2(self,
428451
"input": {
429452
"taskName": task_name,
430453
"filters": {
431-
"projectId": self.uid
454+
"projectId": self.uid,
455+
"searchQuery": {
456+
"scope": None,
457+
"query": []
458+
}
432459
},
433460
"params": {
434461
"includeAttachments":
@@ -446,6 +473,88 @@ def export_v2(self,
446473
},
447474
}
448475
}
476+
477+
if _filters.get('last_activity_at') is not None:
478+
if timezone is None:
479+
timezone = _get_timezone()
480+
values = _filters['last_activity_at']
481+
start, end = values
482+
if (start is not None and end is not None):
483+
[_validate_datetime(date) for date in values]
484+
query_params["input"]["filters"]['searchQuery']['query'].append(
485+
{
486+
"type": "data_row_last_activity_at",
487+
"value": {
488+
"operator": "BETWEEN",
489+
"timezone": timezone,
490+
"value": {
491+
"min": start,
492+
"max": end
493+
}
494+
}
495+
})
496+
elif (start is not None):
497+
_validate_datetime(start)
498+
query_params["input"]["filters"]['searchQuery']['query'].append(
499+
{
500+
"type": "data_row_last_activity_at",
501+
"value": {
502+
"operator": "GREATER_THAN_OR_EQUAL",
503+
"timezone": timezone,
504+
"value": start
505+
}
506+
})
507+
elif (end is not None):
508+
_validate_datetime(end)
509+
query_params["input"]["filters"]['searchQuery']['query'].append(
510+
{
511+
"type": "data_row_last_activity_at",
512+
"value": {
513+
"operator": "LESS_THAN_OR_EQUAL",
514+
"timezone": timezone,
515+
"value": end
516+
}
517+
})
518+
519+
if _filters.get('label_created_at') is not None:
520+
if timezone is None:
521+
timezone = _get_timezone()
522+
values = _filters['label_created_at']
523+
start, end = values
524+
if (start is not None and end is not None):
525+
[_validate_datetime(date) for date in values]
526+
query_params["input"]["filters"]['searchQuery']['query'].append(
527+
{
528+
"type": "labeled_at",
529+
"value": {
530+
"operator": "BETWEEN",
531+
"value": {
532+
"min": start,
533+
"max": end
534+
}
535+
}
536+
})
537+
elif (start is not None):
538+
_validate_datetime(start)
539+
query_params["input"]["filters"]['searchQuery']['query'].append(
540+
{
541+
"type": "labeled_at",
542+
"value": {
543+
"operator": "GREATER_THAN_OR_EQUAL",
544+
"value": start
545+
}
546+
})
547+
elif (end is not None):
548+
_validate_datetime(end)
549+
query_params["input"]["filters"]['searchQuery']['query'].append(
550+
{
551+
"type": "labeled_at",
552+
"value": {
553+
"operator": "LESS_THAN_OR_EQUAL",
554+
"value": end
555+
}
556+
})
557+
449558
res = self.client.execute(
450559
create_task_query_str,
451560
query_params,

tests/integration/test_project.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,32 @@ def test_project_export_v2(configured_project_with_label):
5353
include_performance_details = True
5454
task = project.export_v2(
5555
task_name,
56+
filters={
57+
"last_activity_at": ["2000-01-01 00:00:00", "2050-01-01 00:00:00"],
58+
"label_created_at": ["2000-01-01 00:00:00", "2050-01-01 00:00:00"]
59+
},
5660
params={
5761
"include_performance_details": include_performance_details,
5862
"include_labels": True
5963
})
64+
65+
task_to = project.export_v2(
66+
filters={"last_activity_at": [None, "2050-01-01 00:00:00"]})
67+
68+
task_from = project.export_v2(
69+
filters={"last_activity_at": ["2000-01-01 00:00:00", None]})
70+
6071
assert task.name == task_name
6172
task.wait_till_done()
6273
assert task.status == "COMPLETE"
6374
assert task.errors is None
6475

76+
task_to.wait_till_done()
77+
assert task_to.status == "COMPLETE"
78+
79+
task_from.wait_till_done()
80+
assert task_from.status == "COMPLETE"
81+
6582
for task_result in task.result:
6683
task_project = task_result['projects'][project.uid]
6784
task_project_label_ids_set = set(

0 commit comments

Comments
 (0)