Skip to content

Commit fba3758

Browse files
committed
Add sync_external_project
1 parent b1b5f7c commit fba3758

4 files changed

Lines changed: 148 additions & 0 deletions

File tree

libs/labelbox/src/labelbox/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
)
2323
from labelbox.schema.dataset import Dataset
2424
from labelbox.schema.enums import AnnotationImportState
25+
from labelbox.schema.project_sync import (
26+
AutoQA,
27+
AutoQaStatus,
28+
CustomScore,
29+
ProjectSyncEntry,
30+
ProjectSyncLabel,
31+
ProjectSyncResult,
32+
SubmittedBy,
33+
)
2534
from labelbox.schema.export_task import (
2635
BufferedJsonConverterOutput,
2736
ExportTask,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Backwards-compatibility re-exports. Prefer importing from project_sync."""
2+
3+
from labelbox.schema.project_sync import ( # noqa: F401
4+
AutoQA,
5+
AutoQaStatus,
6+
CustomScore,
7+
ProjectSyncEntry,
8+
ProjectSyncLabel,
9+
ProjectSyncResult,
10+
SubmittedBy,
11+
_to_gql_input,
12+
)
13+
14+
# Deprecated aliases — kept so existing callers don't break immediately.
15+
ExternalMetricsEntry = ProjectSyncEntry
16+
ExternalMetricsResult = ProjectSyncResult

libs/labelbox/src/labelbox/schema/project.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
ProjectExportFilters,
3838
build_filters,
3939
)
40+
from labelbox.schema.project_sync import (
41+
ProjectSyncEntry,
42+
ProjectSyncResult,
43+
_to_gql_input,
44+
)
4045
from labelbox.schema.export_params import ProjectExportParams
4146
from labelbox.schema.export_task import ExportTask
4247
from labelbox.schema.identifiable import DataRowIdentifier
@@ -1001,6 +1006,39 @@ def create_batches(
10011006

10021007
return CreateBatchesTask(self.client, self.uid, batch_ids, task_ids)
10031008

1009+
def sync_external_project(
1010+
self,
1011+
entries: List[ProjectSyncEntry],
1012+
) -> ProjectSyncResult:
1013+
"""Syncs external project data — labels, metrics, and workflow state.
1014+
1015+
Processing is asynchronous. The returned submission ID can be used
1016+
to track the progress of the sync operation.
1017+
1018+
Args:
1019+
entries: A list of ProjectSyncEntry objects.
1020+
1021+
Returns:
1022+
A ProjectSyncResult containing the submission ID.
1023+
"""
1024+
mutation_str = """mutation syncExternalProjectPyApi($input: SyncExternalProjectInput!) {
1025+
syncExternalProject(input: $input) {
1026+
submissionId
1027+
}
1028+
}"""
1029+
1030+
params = {
1031+
"input": {
1032+
"projectId": self.uid,
1033+
"entries": [_to_gql_input(e) for e in entries],
1034+
}
1035+
}
1036+
1037+
response = self.client.execute(mutation_str, params)
1038+
payload = response["syncExternalProject"]
1039+
1040+
return ProjectSyncResult(submission_id=payload["submissionId"])
1041+
10041042
def create_batches_from_dataset(
10051043
self,
10061044
name_prefix: str,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from enum import Enum
2+
from typing import Any, Dict, List, Optional
3+
4+
from pydantic import BaseModel
5+
6+
7+
class AutoQaStatus(str, Enum):
8+
Approve = "Approve"
9+
Reject = "Reject"
10+
Neutral = "Neutral"
11+
12+
13+
class SubmittedBy(BaseModel):
14+
email: str
15+
16+
17+
class CustomScore(BaseModel):
18+
name: str
19+
value: float
20+
21+
22+
class AutoQA(BaseModel):
23+
status: AutoQaStatus
24+
score: Optional[float] = None
25+
feedback: Optional[str] = None
26+
custom_scores: Optional[List[CustomScore]] = None
27+
28+
29+
class ProjectSyncLabel(BaseModel):
30+
submitted_by: SubmittedBy
31+
auto_qa: Optional[AutoQA] = None
32+
seconds_to_completion: Optional[float] = None
33+
submitted_on: Optional[str] = None
34+
35+
36+
class ProjectSyncEntry(BaseModel):
37+
task_id: str
38+
label: Optional[ProjectSyncLabel] = None
39+
queue_type: Optional[str] = None
40+
row_status: Optional[str] = None
41+
42+
43+
class ProjectSyncResult(BaseModel):
44+
submission_id: str
45+
46+
47+
def _to_gql_input(entry: ProjectSyncEntry) -> Dict[str, Any]:
48+
"""Convert a ProjectSyncEntry to a camelCase dict matching the GQL schema."""
49+
result: Dict[str, Any] = {"taskId": entry.task_id}
50+
51+
if entry.label is not None:
52+
label: Dict[str, Any] = {
53+
"submittedBy": {"email": entry.label.submitted_by.email},
54+
}
55+
56+
if entry.label.auto_qa is not None:
57+
auto_qa: Dict[str, Any] = {
58+
"status": entry.label.auto_qa.status.value,
59+
}
60+
if entry.label.auto_qa.score is not None:
61+
auto_qa["score"] = entry.label.auto_qa.score
62+
if entry.label.auto_qa.feedback is not None:
63+
auto_qa["feedback"] = entry.label.auto_qa.feedback
64+
if entry.label.auto_qa.custom_scores is not None:
65+
auto_qa["customScores"] = [
66+
{"name": cs.name, "value": cs.value}
67+
for cs in entry.label.auto_qa.custom_scores
68+
]
69+
label["autoQA"] = auto_qa
70+
71+
if entry.label.seconds_to_completion is not None:
72+
label["secondsToCompletion"] = entry.label.seconds_to_completion
73+
74+
if entry.label.submitted_on is not None:
75+
label["submittedOn"] = entry.label.submitted_on
76+
77+
result["label"] = label
78+
79+
if entry.queue_type is not None:
80+
result["queueType"] = entry.queue_type
81+
82+
if entry.row_status is not None:
83+
result["rowStatus"] = entry.row_status
84+
85+
return result

0 commit comments

Comments
 (0)