diff --git a/libs/labelbox/src/labelbox/client.py b/libs/labelbox/src/labelbox/client.py index 2b43de574..0dee983a6 100644 --- a/libs/labelbox/src/labelbox/client.py +++ b/libs/labelbox/src/labelbox/client.py @@ -2806,7 +2806,7 @@ def get_label_version( Example: >>> version = client.get_label_version("cm2v...") - >>> print(version.action, version.created_by_id, version.created_at) + >>> print(version.action, version.created_by_email, version.created_at) >>> annotations = version.label_json """ return _label_version.get_label_version( diff --git a/libs/labelbox/src/labelbox/schema/label.py b/libs/labelbox/src/labelbox/schema/label.py index a88454cf7..cf65a88e6 100644 --- a/libs/labelbox/src/labelbox/schema/label.py +++ b/libs/labelbox/src/labelbox/schema/label.py @@ -62,7 +62,7 @@ def versions(self, include_annotations: bool = True) -> PaginatedCollection: Example: >>> for version in label.versions(): >>> print(version.created_at, version.action, - >>> version.created_by_id, version.label_json) + >>> version.created_by_email, version.label_json) """ return _label_version.get_label_versions( self.client, diff --git a/libs/labelbox/src/labelbox/schema/label_version.py b/libs/labelbox/src/labelbox/schema/label_version.py index af9f8c454..350459a99 100644 --- a/libs/labelbox/src/labelbox/schema/label_version.py +++ b/libs/labelbox/src/labelbox/schema/label_version.py @@ -77,6 +77,9 @@ class LabelVersion(_CamelCaseMixin): created_at (datetime): When the version was recorded. created_by_id (str): The user who made the hand-off: the labeler for a submit, the reviewer for a review save or decision. + created_by_email (Optional[str]): That user's email, masked the way + other user emails are between a workforce provider and the + organizations it works for. ``None`` when the user can't be read. stage (Optional[LabelVersionStage]): The workflow step the data row was in at the hand-off. ``None`` when it was in none (Done). For ``SENT_TO_REWORK`` this is the step the row was moved out of, so @@ -101,6 +104,7 @@ class LabelVersion(_CamelCaseMixin): action: Union[LabelVersionAction, str] created_at: datetime created_by_id: str + created_by_email: Optional[str] = None stage: Optional[LabelVersionStage] = None annotation_count: int label: Optional[str] = None @@ -109,6 +113,7 @@ class LabelVersion(_CamelCaseMixin): _FIELDS = """ id labelId dataRowId projectId action createdAt createdById + createdBy { email } stage { taskQueueId name type } annotationCount """ @@ -162,8 +167,14 @@ def _to_version(result: Dict[str, Any]) -> LabelVersion: action = result.get("action") if action in LabelVersionAction._value2member_map_: action = LabelVersionAction(action) + created_by = result.get("createdBy") or {} return LabelVersion.model_validate( - {**result, "labelJson": label_json, "action": action} + { + **result, + "labelJson": label_json, + "action": action, + "createdByEmail": created_by.get("email"), + } ) diff --git a/libs/labelbox/src/labelbox/schema/project.py b/libs/labelbox/src/labelbox/schema/project.py index 296223361..6a6a9dfed 100644 --- a/libs/labelbox/src/labelbox/schema/project.py +++ b/libs/labelbox/src/labelbox/schema/project.py @@ -356,7 +356,9 @@ def label_versions( saved from the editor, approved or rejected in a review step, submitted from a rework step, or moved into a rework step. Review and rework edit the same label, so one label's versions show what each - reviewer and rework changed. Autosaves and drafts record none, there + reviewer and rework changed. A review decision on a whole data row + records a version for each label on it, all with the same time and + reviewer. Autosaves and drafts record none, there are none from before recording was enabled for the project, and versions are never changed or deleted. @@ -389,8 +391,9 @@ def label_versions( ValueError: A bad page size or time range. Example: - >>> for version in project.label_versions(label=label_id): - >>> print(version.action, version.created_by_id, + >>> for version in project.label_versions(data_row=data_row_id): + >>> print(version.label_id, version.action, + >>> version.created_by_email, >>> version.stage and version.stage.name) >>> >>> # Everything reviewers did in October, without annotations. diff --git a/libs/labelbox/tests/unit/schema/test_label_version.py b/libs/labelbox/tests/unit/schema/test_label_version.py index fc49c2918..802c9331e 100644 --- a/libs/labelbox/tests/unit/schema/test_label_version.py +++ b/libs/labelbox/tests/unit/schema/test_label_version.py @@ -21,6 +21,7 @@ def node(index, **overrides): "action": "SUBMITTED", "createdAt": f"2026-10-19T10:00:0{index}.000Z", "createdById": "clabeler000000000000000001", + "createdBy": {"email": "labeler@example.com"}, "stage": { "taskQueueId": "b1a2c3d4-0000-0000-0000-000000000001", "name": "Initial labeling task", @@ -78,6 +79,7 @@ def test_pages_by_cursor_and_parses_versions(self): 2026, 10, 19, 10, 0, 1, tzinfo=timezone.utc ) assert first.created_by_id == "clabeler000000000000000001" + assert first.created_by_email == "labeler@example.com" assert first.stage == LabelVersionStage( task_queue_id="b1a2c3d4-0000-0000-0000-000000000001", name="Initial labeling task", @@ -95,6 +97,7 @@ def test_pages_by_cursor_and_parses_versions(self): query1 ) assert "label labelJson" in query1 + assert "createdBy { email }" in query1 assert params1 == { "where": {"projectId": PROJECT_ID}, "first": 2, @@ -161,6 +164,14 @@ def test_keeps_an_unknown_action_as_a_string(self): assert version.action == "NEW_ACTION" + def test_leaves_the_email_empty_when_the_user_is_unreadable(self): + client = client_returning(page([node(1, createdBy=None)])) + + [version] = list(label_version.get_label_versions(client, PROJECT_ID)) + + assert version.created_by_id == "clabeler000000000000000001" + assert version.created_by_email is None + def test_parses_label_json_sent_as_text(self): client = client_returning(page([node(1, labelJson='{"objects": []}')]))